yunUI组件库解析:图片上传与排序组件yImgPro

news2024/10/3 2:15:02

yunUI是笔者开源的微信小程序功能库。目前其中包含了一些复杂的功能组件。方便使用。未来它将分为组件、样式、js三者合为一体,但分别提供。
本文所用代码皆来源于组件库中的yImgPro组件。详细代码可至github查看。地址: yunUI 。
npm地址:yun-ui-micro
欢迎大家star!最近有想法对组件库按照新思路进行重构,各位有什么急切需要或常见使用的组件也欢迎提出!一起共建!

场景如下:
动画排序组件展示

首先分析此需求。有两点:

  1. 拖动时排序
  2. 拖动后排序

单从性能上看,第二个是有优势的。但是从用户体验上看,无疑要选择第一种方案。

除非你的需求是“不能拖动排序”。你可以放心的选择第二种方案。第二种方案在笔者的功能库中也有组件:yImg。本文思路是第一种。

区别于之前写的第二种思路的文章,第一种思路对布局和样式影响很重的点在于:拖动。
拖动时排序意味着这个元素被拖动时不能在原来位置上有“保留”。这很关键,因为我们可以利用“保留”点对第二种方案进行改造使之像第一种效果看齐,但体验上仍有差距。

所以,笔者选择了“定位”position。所有的元素都是定位的,这样拖动时只需要更改transformz-index即可达到效果。记住这一点,这会带来bug,虽然很好解决。
我的wxml决定这样写:

<view class="container">
    <view class="item-wrap" style="height: {{ itemWrapHeight }}px;">
        <view
            class="item {{cur == index? 'cur':''}} {{curZ == index? 'zIndex':''}} {{itemTransition ? 'itemTransition':''}}"
            wx:for="{{list}}" wx:key="{{index}}" id="item{{index}}" data-key="{{item.key}}" data-index="{{index}}"
            style="transform: translate3d({{index === cur ? tranX : item.tranX}}px, {{index === cur ? tranY: item.tranY}}px, 0px);"
            bind:longpress="longPress" catch:touchmove="touchMove" catch:touchend="touchEnd">
            <view class="info" style="width: {{imgShape.side}}rpx; height: {{imgShape.side}}rpx; padding: 0 {{imgShape.pd}}rpx {{imgShape.pd}}rpx 0;">
                <image mode="aspectFill" src="{{item.data}}"></image>
                <i class="iconfont icon-delete" wx:if="{{showMenuImg}}" bind:tap="onDelImage" data-index="{{item.key}}"></i>
            </view>
        </view>
        <view class="item-sel selectphoto"
            style="transform: translate3d({{selSite.tranX}}px, {{selSite.tranY}}px, 0px); width: {{imgShape.side - imgShape.pd}}rpx; height: {{imgShape.side - imgShape.pd}}rpx;" hidden="{{!canSelPhoto}}" bind:tap="onChooseImage">
            <i class="iconfont icon-jiashang"></i>
        </view>
    </view>
</view>

这个组件被变量控制的样式比yImg组件多了许多,但结构上精简了不少。我们来分析下:
首先,因为定位,子元素脱离文档流,所以我们需要动态为item-wrap元素赋height
然后是子元素的宽高和padding也是动态的,没事,我们会在初始化时动态的获取它 —— 顺便添加一些我们需要的数据。

初始化发生在用户选择完图片以后。这个函数中干了几件事:

  1. 初始化数据。将数组-字符串变成数组-对象,存储初始顺序、未来可能的移动距离、图片本身
  2. 获取子项item的宽高(为偏移做准备)、计算当前整个区域高度(所以每次选择完图片都要调用此函数)
  3. 获取图片区域item-wrap的初始信息(位置!)
init() {
    // 遍历数据源增加扩展项, 以用作排序使用
    let list = this.data.listData.map((item, index) => {
        let data = {
            key: index,
            tranX: 0,
            tranY: 0,
            data: item
        }
        return data
    });

    this.setData({
        list: list,
        itemTransition: false
    });

    // 获取每一项的宽高等属性
    this.createSelectorQuery().select(".item").boundingClientRect((res) => {

        let rows;
        let len = this.data.list.length;
        if(len == MAX_IMG_NUM) {
            rows = Math.ceil(len / this.data.columns);
        }else {
            rows = Math.ceil((len + 1) / this.data.columns);
        }

        this.item = res;

        let itemWrapHeight = rows * res.height;

        this.getPosition(this.data.list, false);

        let obj = list[list.length - 1]
        let tranX = res.width * ((obj.key + 1) % this.data.columns);
        let tranY = Math.floor((obj.key + 1) / this.data.columns) * res.height;

        this.setData({
            itemWrapHeight,
            selSite: {
                tranX,
                tranY
            }
        })

        let query = wx.createSelectorQuery().in(this);
        query.select('.item-wrap').boundingClientRect((res) => {
            this.itemWrap = res;

        })
        // 需要的是“距离文档流顶部的距离”。所以咱们需要这片区域已经在页面上滚动了多少了,把这个值加上
        if(this.properties.scrollOffset) {
            this.itemWrap.top += this.properties.scrollOffset;
        }else {
            query.selectViewport().scrollOffset((res) => {
                let _wrap = this.itemWrap.top + res.scrollTop;
                this.itemWrap.top = _wrap;
            })
        }
        
        query.exec()
    }).exec();
},

这里尤其需要注意的是:获取图片区域信息时用的 API 只能获取“当前元素距离屏幕顶部的距离”。而实际大多数情况我们需要的是“当前元素距离文档流顶部的距离”。这两者在一个非常重要的场景下会有大幅偏差 —— 当文档流发生滚动时!
所以笔者采用selectViewport().scrollOffset API 来获得文档流的滚动偏差。并在后面长按甚至拖动过程中将这个偏差“抹去”。

如果你在使用笔者的组件库,并且遇到了“当前组件并不是一开始就在页面上出现而是动态展示”的场景,那么您也可以通过参数将这个“偏差”传入组件。这一点在README的使用说明中也有说明。

因为图片是 position 的,所以哪怕在初始时他们的位置也是计算得到的 —— getPosition 函数。我们根据列数获取每个元素的偏移距离,响应式到他们的transform上:

getPosition(data, vibrate = true) {
    let list = data.map((item, index) => {
        item.tranX = this.item.width * (item.key % this.data.columns);
        item.tranY = Math.floor(item.key / this.data.columns) * this.item.height;
        return item
    });

    this.setData({
        list: list
    });

    if (!vibrate) return;

    let listData = [];

    list.forEach((item) => {
        listData[item.key] = item.data
    });

    this.setData({
        listData,
        itemTransition: true
    })
},

接下来就是长按事件了。
在本组件中,少于两张图片则长按只有删除功能,一定程度上减少性能消耗。
长按时,我们需要拿到当前元素的位置,并且和整体区域位置结合获取“中心点”,并将中心点移动到点击位置处。这也就是我们说的“是否跟手”。并且这样图片的偏移也能提醒用户当前点的是这张图片:

longPress(e) {
    if(this.data.list.length < 2){
        this.setData({
            showMenuImg: true
        });
        wx.vibrateShort();
        return;
    }

    this.setData({
        touch: true
    });

    this.startX = e.changedTouches[0].pageX
    this.startY = e.changedTouches[0].pageY

    let index = e.currentTarget.dataset.index;

    this.tranX = this.startX - this.item.width / 2 - this.itemWrap.left;
    this.tranY = this.startY - this.item.height / 2 - this.itemWrap.top;

    this.setData({
        cur: index,
        curZ: index,
        tranX: this.tranX,
        tranY: this.tranY,
        showMenuImg: true
    });

    wx.vibrateShort();
},

长安之后是拖动。这是我们的核心事件。因为拖动排序,所以我们不仅需要计算当前元素的偏移,还需要计算元素偏移后和“路过元素”的位置关系 —— 临界点判断。

touchMove(e) {
    if (!this.data.touch) return;
    let tranX = e.touches[0].pageX - this.startX + this.tranX,
        tranY = e.touches[0].pageY - this.startY + this.tranY;

    this.setData({
        tranX: tranX,
        tranY: tranY,
        showMenuImg: false
    });

    let originKey = e.currentTarget.dataset.key;

    let endKey = this.calculateMoving(tranX, tranY);

    // 防止拖拽过程中发生乱序问题
    if (originKey == endKey || this.originKey == originKey) return;

    this.originKey = originKey;

    this.insert(originKey, endKey);
},

calculateMoving 函数就是做这个的:

calculateMoving(tranX, tranY) {
    let rows = Math.ceil(this.data.list.length / this.data.columns) - 1,
        i = Math.round(tranX / this.item.width),
        j = Math.round(tranY / this.item.height);

    i = i > (this.data.columns - 1) ? (this.data.columns - 1) : i;
    i = i < 0 ? 0 : i;

    j = j < 0 ? 0 : j;
    j = j > rows ? rows : j;

    let endKey = i + this.data.columns * j;

    endKey = endKey >= this.data.list.length ? this.data.list.length - 1 : endKey;

    return endKey
},

在拖动过程中,每次知道要偏移到哪,也就是抢占哪个元素的位置后,需要根据拖动元素的key 和 “目标元素”的 key 去重新计算每一项的新的key:

insert(origin, end) {
    let list;

    if (origin < end) {
        list = this.data.list.map((item) => {
            if (item.key > origin && item.key <= end) {
                item.key = item.key - 1;
            } else if (item.key == origin) {
                item.key = end;
            }
            return item
        });
        this.getPosition(list);

    } else if (origin > end) {
        list = this.data.list.map((item) => {
            if (item.key >= end && item.key < origin) {
                item.key = item.key + 1;
            } else if (item.key == origin) {
                item.key = end;
            }
            return item
        });
        this.getPosition(list);
    }
},

最后在“松手”时要去把所有在拖动过程中发生变化的变量给恢复初始值:

touchEnd() {
    if (!this.data.touch) {
        return;
    }else {
        this.setData({
            showMenuImg: true
        })
    }
    this.triggerMsg(this.data.listData, "sort-img")
    this.clearData();
},
clearData() {
    this.originKey = -1;

    this.setData({
        touch: false,
        cur: -1,
        tranX: 0,
        tranY: 0
    });

    // 延迟清空
    setTimeout(() => {
        this.setData({
            curZ: -1,
        })
    }, 300)
},

除此之外,还有删除事件。
删除也有两种方案:

  1. 硬删除。删除指定元素后将数组重新初始化init
  2. 有过渡效果的删除。从前到后计算删除元素位置,再从后到前将后一个元素的data赋值给前一个元素。但是其余key、tranX、tranY不变。最后len - 1

第二种方式相当于自己重新算了一遍。从一个地方可以看出两者的区别:需不需要自己计算“上传图片按钮的位置”!在笔者的组件中,也提供了参数可以选择使用哪种方式删除。
(如果放出代码的话整个篇幅就太多了,而且上面其实已经将代码的思路给说完了,“删除功能”的具体代码可以到github上看)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/352352.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Bing+ChatGPT 对传统搜索引擎的降维打击

早些时候申请了新版 Bing 的内测资格&#xff0c;终于收到了通过的邮件。 一天的体验之后&#xff0c;我的感受是&#xff1a;当新版 Bing 具备了 ChatGPT 的聊天能力之后&#xff0c;它的能力不论是对传统搜索引擎&#xff0c;还是 ChatGPT 自身&#xff0c;都将是降维打击。 …

LeetCode 237. 删除链表中的节点

原题链接 难度&#xff1a;middle\color{orange}{middle}middle 题目描述 有一个单链表的 headheadhead&#xff0c;我们想删除它其中的一个节点 nodenodenode。 给你一个需要删除的节点 nodenodenode 。你将 无法访问 第一个节点 headheadhead。 链表的所有值都是 唯一的&…

IoT 边缘集群基于 Kubernetes Events 的告警通知实现(二):进一步配置

上一篇文章 IoT 边缘集群基于 Kubernetes Events 的告警通知实现 目标 告警恢复通知 - 经过评估无法实现原因: 告警和恢复是单独完全不相关的事件, 告警是 Warning 级别, 恢复是 Normal 级别, 要开启恢复, 就会导致所有 Normal Events 都会被发送, 这个数量是很恐怖的; 而且…

【重排重绘】从输入url到浏览器展示页面发生了什么?

目录步骤如下&#xff1a;一、用户在浏览器搜索栏中输入url地址二、浏览器解析域名得到服务器ip地址浏览器解析域名得到服务器ip地址有哪些过程&#xff1f;三、TCP三次握手建立客户端和服务器的连接四、客户端发送HTTP请求获取服务器端的静态资源五、服务器发送HTTP响应报文给…

程序员深度体验一周ChatGPT发现竟然....

程序员深度体验一周ChatGPT发现竟然… 周一打卡上班&#xff0c;老板凑到我跟前&#xff1a;“小李啊&#xff0c;这周有个新需求交给你做一下&#xff0c;给我们的API管理平台新增一个智能Mock的功能…”。我条件反射般的差点脱口而出&#xff1a;“这个需求做不了…”。不过…

【软件测试】资深测试总结的几个自动化测试点,提升跨越一大步......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 自动化的软件测试与…

PostgreSQL查询引擎——SELECT STATEMENTS SelectStmt

SelectStmt: select_no_parens %prec UMINUS| select_with_parens %prec UMINUS select_with_parens:( select_no_parens ) { $$ $2; }| ( select_with_parens ) { $$ $2; } 该规则返回单个SelectStmt节点或它们的树&#xff0c;表示集合操作树(set-operation tree…

JAVA线程池的使用

一、池化思想和JAVA线程池 池化是很重要的思想&#xff1b;池化的好处是提供缓冲和统一的管理。这个笔者在本人的数据库连接池的博客中已经提到过了&#xff08;JAVA常用数据库连接池_王者之路001的博客-CSDN博客 &#xff09;。 线程池是另一种池化思想的运用&#xff0c;把…

MySQL 派生表产生关联索引auto_key0导致SQL非常的慢

相同的SQL在maridb运行0.5秒&#xff0c;在MySQL8.0.26中运行要19秒 官方MySQL在处理子查时&#xff0c;优化器有个优化参数derived_merge&#xff0c;MySQL7开启添加&#xff0c;默认on.很多情况可以自动优化派生表&#xff0c;避免创建临时索引auto_key0和生成临时表数据做…

C++入门:函数重载

目录 一. 函数重载的概念和分类 1.1 什么是函数重载 1.2 函数重载的分类 1.3 关于函数重载的几点注意事项 二. C实现函数重载的底层逻辑&#xff08;为什么C可以实现函数重载而C语言不能&#xff09; 2.1 编译器编译程序的过程 2.2 为什么C可以实现函数重载而C语言不能 …

内网安装管家婆软件如何实现外网访问?内网穿透的几种方案教程

管家婆软件从网络架构上分两种版本&#xff1a;web&#xff08;浏览器http端口&#xff09;访问的版本和客户端&#xff08;211固定端口sqlserver数据库&#xff09;访问的版本。公司库管经常用仓库登录管家婆&#xff0c;一旦需要在公司外部登陆访问管家婆客户端&#xff0c;就…

微信中如何接入机器人才比较正常

大家好,我是雄雄,欢迎关注微信公众号:雄雄的小课堂。 前言 为什么会有这个话题?大家都知道最近有个AI机器人很火,那就是AI机器人,关于它的介绍,大家可以自行百度去,我这边就不多介绍了。 好多人嫌网页版玩的不过瘾,就把这个机器人接入到了QQ上,接入到了钉钉上,TG …

Go语言基础知识学习笔记

环境准备 下载安装Golang&#xff1a;https://golang.google.cn/dl/ 因为国外下载速度较慢&#xff0c;我们需要配置国内代理 # 开启包管理工具 go env -w GO111MODULEon # 设置代理 go env -w GOPROXYhttps://goproxy.cn,direct # 设置不走 proxy 的私有仓库&#xff0c;多…

Ajax?阿贾克斯?

一、Ajax简介 AJAX Asynchronous JavaScript and XML&#xff08;异步的 JavaScript 和 XML&#xff09;。 AJAX 不是新的编程语言&#xff0c;而是一种使用现有标准的创新方法。 AJAX 最大的优点是在不重新加载整个页面的情况下&#xff0c;可以与服务器交换数据并更新部分网…

供应链挑战迎刃而解!桑迪亚国家实验室使出“量子杀手锏”

桑迪亚国家实验室的科学家Alicia Magann&#xff08;右&#xff09;&#xff0c;Kenneth Rudinger&#xff08;左上&#xff09;&#xff0c;Mohan Sarovar&#xff08;左下&#xff09;和Matthew Grace&#xff08;未附图&#xff09;开发了基于反馈的量子优化算法&#xff08…

“太极”如何利用混部资源,助力腾讯广告降本增效

编者按&#xff1a;近年来&#xff0c; 随着大模型在NLP领域横扫各种大数据磅单取得巨大成功之后&#xff0c;大数据加大模型成为了AI领域建模的标准范式。搜索、广告、推荐的建模也不例外&#xff0c;动辄千亿参数&#xff0c;上T大小的模型成为各大预估场景的标配&#xff0c…

人脸识别——景联文科技提供3D头模数据采集业务!

“拿起手机刷脸解锁、上下班考勤、支付订单&#xff0c;刷脸已极大地便利了我们的生活。清华大学新闻学院教授沈阳表示&#xff0c;中国人平均每天要暴露在各种摄像头下超过500次。人脸识别已成了我们生活中重要的一部分。由于2D人脸识别容易受到姿态、表情、光照等因素影响&am…

痛苦面具,140天备战阿里巴巴,一个疏忽让我前功尽弃...

面试是走的内推途径&#xff0c;因为内推的简历通过率远高于其他方式;我的内推的途径有&#xff1a;联系我在字节跳动工作的一个大学学长。 在线面试&#xff0c;有个线上文本编辑器&#xff0c;类似leetcode那种&#xff0c;可以在线编程。然而有点紧张&#xff0c;视频面试网…

C#开发的OpenRA游戏加载界面的实现

C#开发的OpenRA游戏加载界面的实现 游戏的UI是一个游戏必备, 但是游戏的UI都是自己处理的,不能使用像Windows自带的UI。 这样游戏的UI,其实也是使用游戏的方式来显示的, 只不过使用了低帧率的方式来显示。 比如OpenRA游戏界面,就会显示如下: 游戏的界面有很多,先从一个简…

2023美赛F题全部代码+数据+结果 数学建模

2023年美赛F题全部思路 数据代码都已完成 全部内容见链接&#xff1a;https://www.jdmm.cc/file/2708700/ 1.根据文献选的GGDP的指标&#xff0c;发现GGDP与水资源等有关&#xff0c;由此可以筛选出影响GGDP的所有因子&#xff0c;并可以用所有因子利用层次分析法建立评价体…