在实际的移动应用程序交互方式中,最常见的就是滑动操作。像左右滑动切换页面,手指张开来放大图片等,都是由滑动操作来完成的。本文滑动操作组件是指通过手势滑动呼出隐藏的操作按钮。
如下图所示:
|
|
|
| 图1 QQ app | 图2 华为短信app |
以上滑动操作组件具有如下特点:
定义支持滑动操作的组件swipe_item,父组件引用,引用时传递操作按钮、操作按钮样式。滑动操作子组件需要实现手势滑动处理。
代码结构如下:
- <import name="swipeitem" src="../Component/swipe_item.ux"></import>
- <template>
- <div class="container">
- <swipeitem right-options="{{rightBtns}}" left-options="{{leftBtns}}" @swipebtnclick="swipeItemClick">
- <text>左右都有混合滚动</text>
- </swipeitem>
- </div>
- </template>
| 属性 | 类型 | 是否必选 | 默认值 | 描述 |
| identity | Number | 否 | 组件唯一标识,当autoClose为true时,必须要设置 | |
| disabled | Boolean | 否 | false | 是否禁止滑动 |
| autoClose | Boolean | 是 | true | 滑动打开当前组件,是否关闭其他组件 |
| threshold | Number | 否 | 30 | 滑动缺省距离 |
| leftOptions | Array[Option] | 否 | 左侧操作栏内容及样式 | |
| rightOptions | Array[Option] | 否 | 右侧操作栏内容及样式 |
Option对象参数如下:
| 属性 | 类型 | 是否必选 | 描述 |
| txt | String | 否 | 按钮文字 |
| style | Object | 是 | 按钮样式 |
style对象参数如下:
| 属性 | 类型 | 是否必选 | 描述 |
| backgroundColor | Color | 否 | 按钮背景色 |
| txtColor | Color | 否 | 按钮颜色 |
| fontsize | Number | 否 | 文字大小 |
| btnwidth | Number | 是 | 按钮宽度 |
支持的事件
| 事件名称 | 事件传递参数 | 描述 |
| swipebtnclick | {clickIndex:1, position:left|right, btnValue:btnText} | 点击操作栏按钮时触发事件; clickIndex:左或右操作栏上点击的按钮数组索引; position:所点击的操作栏位置,参数值有left和right; btnValue:所点击的按钮文字; |
| centerclick | {} | 点击显示在屏幕中间内容时触发事件 |
| swipestatechange | {isOpened:true|false} | 操作栏打开或关闭时触发 |


|
|
|
| 图3 正常布局效果 | 图4 操作栏按钮默认背景色效果 |
监听手势touchstart、touchmove、touchend事件,计算手指滑动像素,同时调用X轴的平移动画animate方法,实现组件的移动效果。
当点击子组件操作栏上的按钮时,需要父组件处理业务逻辑,通过$emit()向父组件发送通知。注意,不要在子组件中处理业务逻辑,否则代码无法维护。
1) 操作栏按钮点击事件通知
2)中间内容部分点击事件通知
3)操作栏打开、关闭事件通知
相关代码如下:


滑动打开当前组件时,是否关闭其他组件,当autoClose设置为true时,identity属性必须设置,且唯一标识当前子组件。
滑动操作子组件swipe_item.ux
- <template>
- <!-- Only one root node is allowed in template. -->
- <div class="container" style="{{containerStyle}}" >
- <div id="swipe_item" class="swipe" ontouchstart="dealTouchStart" ontouchmove="dealTouchMove" ontouchend="dealTouchEnd" >
- <div class="left" id="leftbtns">
- <block for="(index,item) in leftOptions">
- <input type="button" class="btn"
- style="background-color: {{calBtnBgColor(item.style.backgroundColor)}};
- color:{{calBtnTxtColor(item.style.txtColor)}};
- font-size:{{calBtnFontSize(item.style.fontsize)}}px;width:{{item.style.btnwidth}}px;"
- value="{{item.txt}}" onclick="btnsClick(index,'left',item.txt)">
- </input>
- </block>
- </div>
- <div class="center" style="width:{{deviceLogicWidth}}px" onclick="dealClickCenter">
- <slot ></slot>
- </div>
-
- <div class="right" id="rightbtns">
- <block for="(index,item) in rightOptions">
- <input type="button" class="btn"
- style="background-color: {{calBtnBgColor(item.style.backgroundColor)}};
- color:{{calBtnTxtColor(item.style.txtColor)}};
- font-size:{{calBtnFontSize(item.style.fontsize)}}px;width:{{item.style.btnwidth}}px;"
- value="{{item.txt}}" onclick="btnsClick(index,'right',item.txt)">
- </input>
- </block>
- </div>
- </div>
- </div>
- </template>
-
- <style>
- .container {
- flex-direction: column;
- justify-content: center;
- }
- .swipe {
- flex-direction: row;
- align-items: center;
- }
-
- .txt {
- font-size: 40px;
- width: 100%;
- height: 100%;
- }
-
- .btn {
- height: 100%;
- }
-
- .left {
- flex-direction: row;
- flex-shrink: 0;
- height: 100%;
- background-color: #a9a9a9;
- }
-
- .center {
- flex-direction: row;
- align-items: center;
- padding: 16px;
- flex-shrink: 0;
- }
-
- .right {
- flex-direction: row;
- flex-shrink: 0;
- height: 100%;
- background-color: #a9a9a9;
- }
- </style>
-
- <script>
- import device from '@system.device';
- const defaultBtnBg="#f01f1f";
- const defaultBtnFontSize=32;
- const defaultBtnTxtColor="#dcdcdc";
-
- module.exports = {
- props: {
- // 禁用
- disabled: {
- type: Boolean,
- default: false
- },
-
- identity:{
- type: String,
- default: '0',
- },
-
- // 是否自动关闭
- autoClose: {
- type: Boolean,
- default: true
- },
-
- // 滑动缺省距离
- threshold: {
- type: Number,
- default: 30
- },
-
- // 左侧按钮内容
- leftOptions: {
- type: Array,
- default() {
- return []
- }
- },
-
- // 右侧按钮内容
- rightOptions: {
- type: Array,
- default() {
- return []
- }
- }
- },
- data: {
- movestartX: 0,
- lastMoveX:0,
- translateEndX: 0,
- leftbtnsWidth: 0,
- rightbtnsWidth: 0,
- deviceLogicWidth:750,
- isLeftOpened: false,
- isRightOpened: false
- },
- computed: {
- containerStyle() {
- console.info("computed containerStyle begin")
- var style = '';
- let totalwidth=0;
- let leftwidth=0;
- this.leftOptions.forEach((item, index, array) => {
- console.info("computed item="+JSON.stringify(item));
- leftwidth+=item.style.btnwidth;
- })
- this.rightOptions.forEach((item, index, array) => {
- totalwidth+=item.style.btnwidth;
- })
-
-
- totalwidth=totalwidth+this.deviceLogicWidth+leftwidth;
- console.info("computed totawidth="+totalwidth+", left width="+leftwidth);
- style += 'width:' + totalwidth + 'px;'
- style += 'transform: translateX(-' + leftwidth + 'px);'
- return style;
- }
- },
-
- calBtnBgColor(color){
- if(color!==undefined){
- return color;
- }
- return defaultBtnBg;
- },
-
- calBtnFontSize(size){
- if(size!==undefined){
- return size;
- }
- return defaultBtnFontSize;
- },
-
- calBtnTxtColor(color){
- if(color!==undefined){
- return color;
- }
- return defaultBtnTxtColor;
- },
-
-
- onInit() {
- console.info("oninit()");
- this.$on('closeSwipeItem', this.closeFromParent);
- setTimeout(() => {
- this.calWidth();
- }, 500);
-
- },
-
- calWidth() {
- this.calLeftBtnsWidth();
- this.calRightBtnsWidth();
- },
-
- calLeftBtnsWidth() {
- var that = this;
- this.$element('leftbtns').getBoundingClientRect({
- success(res) {
- let msg = JSON.stringify(res);
- console.log('calLeftBtnsWidth 当前坐标:' + msg);
- // that.leftbtnsWidth = res.width;
- that.convertRealPx(true, res.width);
- },
- fail() {
- console.log('calBoxWidth 获取失败');
- },
- complete() {
- console.log('calBoxWidth complete')
- }
- })
- },
-
- calRightBtnsWidth() {
- var that = this;
- this.$element('rightbtns').getBoundingClientRect({
- success(res) {
- let msg = JSON.stringify(res);
- console.log('calRightBtnsWidth 当前坐标:' + msg);
- // that.rightbtnsWidth = res.width;
- that.convertRealPx(false, res.width);
- },
- fail() {
- console.log('calBoxWidth 获取失败');
- },
- complete() {
- console.log('calBoxWidth complete')
- }
- })
- },
-
- convertRealPx(isLeftWidth, data) {
- var d = device.getInfoSync();
- console.info("calBannerPostion1 d= " + JSON.stringify(d));
-
- //获取页面内可见窗口的高度和宽度,此值不包括标题栏和状态栏高度
- let windowWidth = d.windowWidth;
- //logicWidth对应manifest.json文件设置的designWidth值,默认是750
- let logicWidth = d.windowLogicWidth;
- let result = data * 1.0 * windowWidth / logicWidth;
- if (isLeftWidth) {
- this.leftbtnsWidth = result;
- } else {
- this.rightbtnsWidth = result;
- }
- },
-
- dealTouchStart: function (e) {
- if (this.disabled) {
- return;
- }
- this.movestartX = e.touches[0].clientX;
- console.info("dealTouchStart movestartX="+this.movestartX);
- },
-
- dealTouchMove: function (e) {
- if (this.disabled) {
- return;
- }
- let moveX = e.touches[0].clientX;
- let dis = moveX - this.movestartX;
- console.info("dealTouchMove moveX= " + moveX + ", dis=" + dis);
- if (Math.abs(dis) < this.threshold) {
- return;
- }
-
- if (dis > 0) {
- //右滑动呼出左边的按钮或者隐藏右边按钮恢复初始状态
- if (this.isRightOpened) {
- //隐藏右边的按钮,恢复初始状态
- this.animate(dis-this.rightbtnsWidth, dis);
- } else {
- //右滑动,呼出左边的按钮
- if (this.leftbtnsWidth > 0) {
- if (!this.isLeftOpened) {
- console.info("begin to show the left buttons");
- this.animate(dis, dis);
- }
- }
- }
- }
- else if (dis < 0) {
- console.info("dealTouchMove left this.isLeftOpened=" + this.isLeftOpened);
- if (this.isLeftOpened) {
- //慢慢滑动将左边按钮隐藏
- this.animate(this.leftbtnsWidth + dis, dis);
- } else {
- //呼出右边的按钮
- if (this.rightbtnsWidth > 0) {
- if (!this.isRightOpened) {
- console.info("dealTouchMove begin to show the right buttons");
- this.animate(dis, dis);
- }
- }
- }
- }
- },
-
- dealTouchEnd: function (e) {
- if (this.disabled) {
- return;
- }
- let endX = e.changedTouches[0].clientX;
- let dis = endX - this.movestartX;
- if (Math.abs(dis) < this.threshold) {
- return;
- }
- console.info("dealTouchEnd dis=" + dis + ", endX=" + endX + ", isLeftOpened=" + this.isLeftOpened + ",isRightOpened=" + this.isRightOpened);
- if (dis > 0) {
- //往右边滑动
- if (this.isRightOpened) {
- //隐藏右边按钮
- this.animate(dis - this.rightbtnsWidth, 0);
- this.isRightOpened = false;
- this.$emit('swipestatechange', {params: {"isOpened":false}});
-
- } else {
- if (!this.isLeftOpened) {
- //呼出左边按钮,将左边按钮完整显示出来;
- if (this.leftbtnsWidth > 0) {
- this.animate(dis, this.leftbtnsWidth);
- this.isLeftOpened = true;
- this.$emit('swipestatechange', {params: {"isOpened":true}});
- }
- }
- }
-
- } else if (dis < 0) {
- //往左滑动
- if (this.isLeftOpened) {
- //隐藏左边按钮
- this.animate(this.leftbtnsWidth + dis, 0);
- this.isLeftOpened = false;
- this.$emit('swipestatechange', {params: {"isOpened":false}});
- } else {
- if (this.rightbtnsWidth > 0 && !this.isRightOpened) {
- //呼出右边按钮,将右边按钮完整显示出来
- this.animate(dis, -this.rightbtnsWidth);
- this.isRightOpened = true;
- this.$emit('swipestatechange', {params: {"isOpened":true}});
- }
- }
- }
- },
-
- animate(value1, value2) {
- console.info("aninate translateX from: " + value1 + ", to:" + value2);
- let cmp = this.$element('swipe_item');
- var options = {
- duration: 300, easing: 'linear',
- delay: 0,
- fill: 'forwards'
- }
-
- console.info("dealTouchMove value2=" + value2);
- var frames = [
- {
- transform: {
- translateX: value1,
- }
- },
- {
- transform: {
- translateX: value2,
- }
- }];
- var animation = cmp.animate(frames, options);
- animation.play();
- animation.onfinish = function () {
- console.log("animation onfinish");
-
- }
- animation.oncancel = function () {
- console.log("animation oncancel");
- }
-
- },
-
- btnsClick: function(index,direction,btnValue) {
- console.info("swipe.ux item click direction="+direction);
- this.$emit('swipebtnclick', {params: {"clickIndex":index,"position":direction,"btnValue":btnValue}});
- //按钮关闭
- if(direction=='left'){
- this.animate(this.leftbtnsWidth, 0);
- this.isLeftOpened = false;
- }else{
- this.animate( - this.rightbtnsWidth, 0);
- this.isRightOpened = false;
- }
- this.$emit('swipestatechange', {params: {"isOpened":false}});
- },
-
- close:function() {
- if(this.isLeftOpened){
- //关闭
- this.animate(this.leftbtnsWidth, 0);
- this.isLeftOpened = false;
- this.$emit('swipestatechange', {params: {"isOpened":false}});
- }else if(this.isRightOpened){
- //关闭
- this.animate( - this.rightbtnsWidth, 0);
- this.isRightOpened = false;
- this.$emit('swipestatechange', {params: {"isOpened":false}});
- }else{
- //中间部分响应自身的点击事件
- this.$emit('centerclick', {params: {}});
- }
- },
-
- closeFromParent:function(e) {
- console.info("closeFromParent e="+JSON.stringify(e));
- if(!this.autoClose){
- return;
- }
-
- //最新滑动打开的操作栏不关闭,其他的才关闭
- let excludeCloseId=e.detail.exclude;
- if(excludeCloseId==this.identity){
- return;
- }
-
- if(this.isLeftOpened){
- //关闭
- this.animate(this.leftbtnsWidth, 0);
- this.isLeftOpened = false;
- }else if(this.isRightOpened){
- //关闭
- this.animate( - this.rightbtnsWidth, 0);
- this.isRightOpened = false;
- }
- },
-
- dealClickCenter: function() {
- console.info("dealClickCenter");
- this.close();
- },
- }
- </script>
-
- 页面main.ux
- <import name="swipeitem" src="../Component/swipe_item.ux"></import>
- <template>
- <!-- Only one root node is allowed in template. -->
- <div class="container">
- <div class="section">
- <text class="sec_txt">只有左边</text>
- </div>
- <swipeitem identity='111' left-options="{{leftBtns}}" @swipebtnclick="swipeItemClick" @swipestatechange="swipeItemChange('111')">
- <text>只有左边按钮</text>
- </swipeitem>
-
- <div class="section">
- <text class="sec_txt">只有右边</text>
- </div>
- <swipeitem identity='112' right-options="{{rightBtns}}" @swipebtnclick="swipeItemClick" @swipestatechange="swipeItemChange('112')">
- <text>只有右边按钮</text>
- </swipeitem>
-
- <div class="section">
- <text class="sec_txt">混合滚动</text>
- </div>
- <swipeitem identity='113' right-options="{{rightBtns}}" left-options="{{leftBtns}}" @swipebtnclick="swipeItemClick" @swipestatechange="swipeItemChange('113')">
- <text>左右都有混合滚动</text>
- </swipeitem>
-
- <div class="section">
- <text class="sec_txt">禁止左右滚动</text>
- </div>
- <swipeitem disabled="{{disabled}}" left-options="{{leftBtns}}" right-options="{{rightBtns}}" @swipebtnclick="swipeItemClick">
- <text>禁止左右滚动</text>
- </swipeitem>
-
- <div class="section">
- <text class="sec_txt">div列表</text>
- </div>
-
- <div class="swipelist" id="swiplist">
- <swipeitem identity="{{item.id}}" auto-close={{isautoclose}} right-options="{{item.options}}" for="(index,item) in swipeList" @swipestatechange="listSwipeItemChange(index)"
- @swipebtnclick="listSwipeItemClick(index)" @centerclick="listCenterItemClick(index)">
- <text onclick="clickText">{{item.content}}</text>
- </swipeitem>
- </div>
- </div>
- </template>
-
- <style>
- .container {
- flex-direction: column;
- width: 100%;
- }
-
- .section {
- background-color: #f5f5f5;
- height: 100px;
- width: 100%;
- border: 1px solid #f5f5f5;
- }
-
- .swipelist {
- flex-direction: column;
- }
-
- .istItemStyle {
- width: 1300px;
- }
-
- .liststyle {
- flex-direction: column;
- }
-
- .sec_txt {
- font-size: 32px;
- font-weight: 600;
- margin-left: 25px;
- margin-top: 10px;
- }
- </style>
-
- <script>
- import prompt from '@system.prompt';
- module.exports = {
- data: {
- isautoclose:true,
- leftBtns: [],
- disabled: true,
- rightBtns: [],
- unstyleleftBtns: [],
- unstylerightBtns: [],
- swipeList: []
- },
- onInit: function () {
- let btn1 = { "txt": "置顶", "style": { "backgroundColor": "#00bfff", "txtColor": "#dcdcdc", "fontsize": 30, "btnwidth": 150 } };
- let btn2 = { "txt": "ok", "style": { "backgroundColor": "#00bfff", "txtColor": "#dcdcdc", "fontsize": 30, "btnwidth": 200 } };
- let btn3 = { "txt": "取消置顶", "style": { "backgroundColor": "#f01f1f", "txtColor": "#dcdcdc", "fontsize": 30, "btnwidth": 150 } };
- let btn4 = { "txt": "cancel", "style": { "backgroundColor": "#00bfff", "txtColor": "#dcdcdc", "fontsize": 30, "btnwidth": 180 } };
-
- let unstylebtn1 = { "txt": "置顶", "style": { "btnwidth": 230 } };
- let unstylebtn2 = { "txt": "ok", "style": { "btnwidth": 200 } };
- let unstylebtn3 = { "txt": "取消置顶", "style": { "btnwidth": 150 } };
- let unstylebtn4 = { "txt": "cancel", "style": { "txtColor": "#ff9900", "fontsize": 30, "btnwidth": 150 } };
- this.leftBtns.push(btn1);
- this.leftBtns.push(btn2);
- this.rightBtns.push(btn3);
- this.rightBtns.push(btn4);
-
- this.unstyleleftBtns.push(unstylebtn1);
- this.unstyleleftBtns.push(unstylebtn2);
-
- this.unstylerightBtns.push(unstylebtn3);
- this.unstylerightBtns.push(unstylebtn4);
-
- },
-
- onReady(options) {
- this.swipeList = [{
- options: [{
- txt: '添加',
- style: {
- backgroundColor: 'rgb(255,58,49)',
- txtColor: "#dcdcdc",
- fontsize: 36,
- btnwidth: 150
- }
- }],
- id: '10',
- content: 'item1'
- },
- {
- id: '11',
- options: [{
- txt: '添加',
- style: {
- backgroundColor: 'rgb(255,58,49)',
- txtColor: "#dcdcdc",
- fontsize: 30,
- btnwidth: 150
- }
- },
- {
- txt: '删除',
- style: {
- backgroundColor: 'rgb(255,58,49)',
- txtColor: "#dcdcdc",
- fontsize: 30,
- btnwidth: 150
- }
- }
- ],
- content: 'item2'
- },
- {
- id: '12',
- options: [{
- txt: '置顶',
- style: {
- backgroundColor: 'rgb(255,58,49)',
- txtColor: "#dcdcdc",
- fontsize: 30,
- btnwidth: 150
- }
- },
- {
- txt: '标记为已读',
- style: {
- backgroundColor: 'rgb(255,58,49)',
- txtColor: "#dcdcdc",
- fontsize: 30,
- btnwidth: 150
- }
- },
- {
- txt: '删除',
- style: {
- backgroundColor: 'rgb(255,58,49)',
- txtColor: "#dcdcdc",
- fontsize: 30,
- btnwidth: 150
- }
- }
- ],
- content: 'item3'
- }
- ]
- },
- listSwipeItemClick: function (index, e) {
- console.info("main swipeItemClick e:" + JSON.stringify(e) + ",index=" + index);
- let position = e.detail.params.position;
- // // let index = e.detail.params.clickIndex;
- // let itemListId = e.detail.params.itemListIndex;
- let btnValue = e.detail.params.btnValue;
- let msg = '';
- if (position == 'left') {
- // msg='点击了左侧 ${e.detail.params.btnValue}按钮 ' ;
- msg = '点击了左侧' + btnValue + '按钮 ';
- } else {
- msg = '点击了右侧' + btnValue + '按钮 ';
- }
- prompt.showToast({
- message: msg,
- duration: 2000,
- gravity: 'center'
- })
-
- if (btnValue == "添加") {
- console.info("begin to add ")
- this.addSwipteItem();
- } else if (btnValue == "删除") {
- this.delSwipteItem(index);
- }
- },
-
- addSwipteItem: function () {
- this.swipeList.push({
- id: new Date().getTime(),
- content: '新增' + new Date().getTime(),
- options: [{
- txt: '置顶',
- style: {
- fontsize: 30,
- btnwidth: 150
- }
- },
- {
- txt: '标记为已读',
- style: {
- backgroundColor: 'rgb(254,156,1)',
- fontsize: 30,
- btnwidth: 150
- }
- },
- {
- txt: '删除',
- style: {
- backgroundColor: 'rgb(255,58,49)',
- fontsize: 30,
- btnwidth: 150
- }
- }
- ],
-
- });
- },
-
- delSwipteItem: function (index) {
- var that = this;
- prompt.showDialog({
- title: '提示',
- message: '是否删除',
- buttons: [
- {
- text: '确定',
- color: '#33dd44'
- }, {
- text: '取消',
- color: '#33dd44'
- }],
- success: function (data) {
- console.log("delSwipteItem showDialog handling callback data:" + JSON.stringify(data));
- if (data.index == 0) {
- //delete
- that.swipeList.splice(index, 1);
- }
-
- },
- fail: function (data, code) {
- console.log("handling fail, code = " + code);
- }
- })
- },
- swipeItemClick: function (e) {
- console.info("main swipeItemClick e:" + JSON.stringify(e));
- let position = e.detail.params.position;
- let btnValue = e.detail.params.btnValue;
- let msg = '';
- if (position == 'left') {
- // msg='点击了左侧 ${e.detail.params.btnValue}按钮 ' ;
- msg = '点击了左侧' + btnValue + '按钮 ';
- } else {
- msg = '点击了右侧' + btnValue + '按钮 ';
- }
- prompt.showToast({
- message: msg,
- duration: 2000,
- gravity: 'center'
- })
-
- },
- clickText: function () {
- console.info("click text");
- prompt.showToast({
- message: 'click text',
- duration: 2000,
- gravity: 'center'
- })
- },
- listCenterItemClick: function (index, e) {
- console.info("main listCenterItemClick e:" + JSON.stringify(e) + ",index=" + index);
- prompt.showToast({
- message: 'click text',
- duration: 2000,
- gravity: 'center'
- })
-
- },
- listSwipeItemChange: function (index, e) {
- console.info("main listSwipeItemChange e:" + JSON.stringify(e) + ",index=" + index);
- this.$broadcast('closeSwipeItem', { 'exclude': this.swipeList[index].id });
- },
- swipeItemChange: function(id,e) {
- console.info("main swipeItemChange e:" + JSON.stringify(e)+",id="+id);
- this.$broadcast('closeSwipeItem', { 'exclude': id });
-
- },
- }
- </script>
欲了解更多更全技术文章,欢迎访问https://developer.huawei.com/consumer/cn/forum/?ha_source=zzh