微信小程序手把手教你实现带字母索引的城市选择列表

前言

在做Android和iOS应用开发的时候,我们经常在app中要用到带字母索引的城市选择功能,他们功能基本都大同小异,大概长下面的样子:
在这里插入图片描述
那么在微信小程序(文章后面统称小程序)中我们要怎么实现类似的功能了?今天就手把手教大家撸一遍代码。
友情提示:后面文章会用到一些布局技巧,我在之前的布局技巧系列文章中有提到,有兴趣的可以回头去看看:

需求分析

在这里插入图片描述
通过对界面的元素分析,我们需要实现的内容如下:

  • 左边可滑动列表
    • 滑动列表UI实现(头部是定位城市,下面是城市列表,城市列表根据首字母进行分类展示)
    • 点击item能得到对应item的信息(城市id和名字等)
  • 右边带字母的索引条
    • 索引条从上到下分别是定位和26个大写字母
    • 索引条能响应触摸和点击事件,屏幕中心显示当前索引
    • 触摸或者点击相应的区域能和滚动列表进行联动

分析完上面的需求,下面我们就拆分这两块,各个击破。

首先我们要做的就是先画出UI,通过观看上面的gif图片,我们很容易得出这样一个结论:整个界面是一个水平左右布局,滑动列表在左边,索引条在屏幕右边的一个竖向布局,然后通过遍历一个数组进行循环遍历。

左边可滑动列表

滑动列表UI实现

滑动列表我们这里使用scroll-view,然后设置为竖直方向滚动。至于为什么要用这个控件,是因为需要和右边索引条做联动,而scroll-view恰好提供了scroll-into-view这个属性让其滚动到指定位置。下面我们看实现代码:

// wxml布局文件
<view class='content'>
  <scroll-view scroll-y='true' class='city-scroll' scroll-with-animation='true'>
    <view class='city-content'>
      //左边滑动列表头部
      <view class='location-city-title'>定位城市</view>
      <view class='location-parent'>
        <view class='location-city'>南昌市</view>
      </view>
      //城市列表
      <view wx:for='{{citys}}' class='city-item'>
      	//这里通过条件控制来决定显示字母还是城市名字
        <text class='city-letter' wx:if='{{item.isshowletter}}'>{{item.simplepinyin}}</text>
        <text class='city-name'>{{item.name}}</text>
      </view>
    </view>
  </scroll-view>
  <view class='right'></view>
</view>
// wxss布局文件
//水平布局,子元素从右到左排列
.content {
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  height: 100%;
}
//scroll-view固定左边690rpx宽度
.city-scroll {
  left: 0;
  position: fixed;
  height: 100%;
  width: 690rpx;
}
//scroll-view子元素竖向布局
.city-content {
  display: flex;
  flex-direction: column;
}
.location-parent {
  display: flex;
  flex-direction: row;
}
.location-city-title {
  padding-left: 40rpx;
  padding-top: 30rpx;
  padding-bottom: 30rpx;
  background: rgba(223, 222, 222,0.5);
  color: gray;
  font-size: 30rpx;
}
.location-city {
  border: 1rpx solid #ccc;
  border-radius: 20rpx;
  margin-left: 40rpx;
  margin-top: 20rpx;
  padding-top: 15rpx;
  padding-bottom: 15rpx;
  font-size: 30rpx;
  width: 150rpx;
  text-align: center;
}
//城市列表item
.city-item {
  display: flex;
  flex-direction: column;
}
//字母
.city-letter {
  font-size: 30rpx;
  padding-top: 30rpx;
  padding-left: 40rpx;
}
//城市名字
.city-name {
  border-bottom: 1px solid #ccc;
  background: rgba(223, 222, 222,0.5);
  padding-top: 20rpx;
  padding-bottom: 20rpx;
  font-size: 30rpx;
  color: gray;
  padding-left: 40rpx;
}
//右侧索引条布局,先设置背景颜色占位
.right {
  position: fixed;
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 60rpx;
  background: red;
}
// js文件
var items = require('../../data/citydata.js');//引入我们的城市列表资源
var that;
//增加属性
data: {
    letters: ['定位', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
    citys: []//增加属性
  }
  //onload函数中赋值
  that = this,
  that.setData({
    citys: items.citys
  })

citydata.js这个文件是在和项目pages同级目录data下面,数据结构如下:

// js文件
const citys = [
  {
    "orgId": "360829",//城市id
    "name": "安福县(吉安市)",//城市名字
    "simplepinyin": "A",//首字母
    "isshowletter": true//是否显示首字母
  },
  ...//省略若干
  ]
  //暴露给外部使用
  module.exports = {
  citys: citys
  }

item点击事件

在xwml中增加bindtap事件,代码如下:

// wxml布局文件
<view class='content'>
  <scroll-view scroll-y='true' class='city-scroll' scroll-with-animation='true'>
    ...
      //城市列表,增加点击事件,把id和名字通过data-xxx形式传递到js
      <view wx:for='{{citys}}' class='city-item' bindtap='selectcity' data-orgid='{{item.orgId}}' data-orgname='{{item.name}}'>
      	//这里通过条件控制来决定显示字母还是城市名字
        <text class='city-letter' wx:if='{{item.isshowletter}}'>{{item.simplepinyin}}</text>
        <text class='city-name'>{{item.name}}</text>
      </view>
   ...
</view>

js中响应事件

// js文件
selectcity: function(e) {
    var orgid = e.currentTarget.dataset.orgid
    var orgname = e.currentTarget.dataset.orgname
    wx.showToast({
      title: 'orgid : ' + orgid + ' orgname : ' + orgname,
      icon: 'none'
    })
  }

代码看上去很多,其实也不难,对照界面布局和注释,我相信看到这里应该没有啥难度,接下来我们看下效果
在这里插入图片描述

到这里左边的滑动列表我们就实现了,城市列表数据来源读者大可以按照自己的需求替换,只要格式跟上面类似就可以

右边带字母的索引条

索引条从上到下分别是定位和26个大写字母

我们先看wxml布局

// wxml布局文件
<view class='content'>
  ...//左边滑动列表布局先省略
  <view class='right'>
    <view wx:for="{{letters}}" class='letter'>{{item}}</view>
  </view>
</view>

js文件

// js布局文件
data: {
    letters: ['定位', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
  }

wxss文件

// wxss文件,水平布局,子元素从右到左排列
.content {
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  height: 100%;
}
//右侧索引条采用fixed固定,并采用sace-around让索引条里的子元素间隔均匀分布
.right {
  position: fixed;
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 60rpx;
  justify-content: space-around;
  align-items: center;
}
.letter {
  font-size: 20rpx;
  color: #1296DB;
  width: 60rpx;
  padding-top: 5rpx;
  padding-bottom: 5rpx;
  text-align: center;
}

上面代码和注释已经标注的很清楚了,我们去掉索引条原来占位的红色背景之后看下效果:

在这里插入图片描述
看上去效果还不错,已经达到了我们预期

索引条响应触摸和点击事件

接下来就是响应索引条的触摸和点击事件,这里应该就是整篇文章中稍微难点的地方了。那么我们怎么知道当前我们的手指触摸到了那个字母上面了呢?我们的思路是,将索引条从上到下等分为27份(里面26个字母和一个定位,总共27个元素),当手指触摸的时候记下y坐标,然后用这个y坐标除以索引条的高度再乘以27,最后转成整数,这个就是我们手指触摸在letters数组中的下标了。具体公式为:
index = parseInt(touchy / height * 27)
接下来代码实现:

// wxml布局文件
<view class='content'>
  ...//省略部分代码
  <view class='right' bindtouchstart='touchStart' bindtouchmove='touchMove' bindtouchend='touchEnd' id='right'>
    <view wx:for="{{letters}}" class='letter' bindtap='letterclick' data-letter="{{item}}">{{item}}</view>
  </view>
</view>
// js文件
var touchEndy = 0;//页面增加y坐标属性定义
var rightheight = 0;//索引条高度

//onshow中获取索引条高度
var query = wx.createSelectorQuery();//创建节点选择器
    query.select('#right').boundingClientRect()
    query.exec(function (res) {
      //res就是 所有标签为mjltest的元素的信息 的数组
      console.log(res);
      //取高度
      console.log("height : "+res[0].height);
      rightheight = res[0].height;
 })    

//开始触摸事件
touchStart: function (e) {
  console.log('touchStart start ');
  touchEndy = e.touches[0].pageY;
  console.log('touchStart end ');
},
touchMove: function (e) {
  touchEndy = e.touches[0].pageY;
  var lindex = parseInt(touchEndy / rightheight * 27);//根据前面分析获取手指触摸位置在letters中的index值
  var value = this.data.letters[lindex];
  console.log(" touchMove value : " + value);
},
touchEnd: function (e) {
   var lindex = parseInt(touchEndy / rightheight * 27);
   var value = this.data.letters[lindex];
   console.log("touchEnd value: " + value);
 },
  //右侧索引表点击事件
  letterclick: function (e) {
    var letter = e.currentTarget.dataset.letter;
    
    if('定位' == letter){
      that.setData({
        toView: 'dw',
      })
    }else{
      this.showOrHideLetterDialog(isShow,letter,true);
      that.setData({
        toView: letter,
      })
    }
    console.log('letterclick letter : ' + letter);
  }

上面这段代码核心就是把触摸的坐标转换成letters中的下标,从而拿到letters中的内容,进而通过弹框显示,到这里,通过控制台打印,我们能正确的将触摸坐标映射成letters中的下标,我就不截图展示了。接下来就是把拿到的letters中内容通过弹框展示出来,主要是通过自定义组件来实现,组件在项目中和pages平级的componet目录中的letterDialog目录中(这个目录可以自己定义的),下面直接看组件实现:

// js文件
// component/commDialog/commDialog.js
Component({
  /**
   * 组件的属性列表
   */
  properties: {
  },

  /**
   * 组件的初始数据
   */
  data: {
    isShow: false,//是否显示
  },

  /**
   * 组件的方法列表
   */
  methods: {
    hideDialog: function () {
      this.setData({
        isShow: false
      });
    },
    showDialog: function () {
      this.setData({
        isShow: true
      });
    },
    setLetter: function (l){
      this.setData({
        letter: l
      });
    },
    getDialogState: function () {
      return this.data.isShow;
    }
  },
})

// json文件
{
  "component": true,
}
// wxml文件
<view class='letter-text' wx:if="{{isShow}}">{{letter}}</view>
// wxss文件
.letter-text {
  background: white;
  color: #1296DB;
  font-size: 100rpx;
  font-weight: bold;
  position: fixed;
  width: 150rpx;
  padding-top: 30rpx;
  padding-bottom: 30rpx;
  top: 40%;
  left: 300rpx;
  text-align: center;
  box-shadow:0px 2px 5px 5px gray;//增加阴影
}

然后我们在页面json文件中引用组件:

// 页面json文件,这里路径要注意
{
  "usingComponents": {
    "dialog": "../../component/letterDialog/letterDialog"
  },
  "navigationBarTitleText": "城市选择"
}

页面wxml增加dialog布局

// 页面wxml
<view class='content'>
  ...//隐藏布局
  <dialog id='dialog'/>
</view>

页面js获取dialog,并在touch事件和点击事件中弹出

// 页面js
onReady: function () {
    //获得dialog组件
    this.dialog = this.selectComponent("#dialog");
}

//右侧索引表点击事件
  letterclick: function (e) {
    var letter = e.currentTarget.dataset.letter;
    var isShow = that.dialog.getDialogState();
    if('定位' == letter){//点击定位不弹框
    }else{
      //不是点击定位,弹出触摸的字母
      this.showOrHideLetterDialog(isShow,letter,true);
    }
    console.log('letterclick letter : ' + letter);
  },
  startTime: function (autodimiss) {
    //1500毫秒之后弹框自动消失
    if (autodimiss){
      timer = setTimeout(function () {
        that.dialog.hideDialog();
      }, 1500)
    }
  },  //touch 事件有bug
  touchStart: function (e) {
    console.log('touchStart start ');
    touchEndy = e.touches[0].pageY;
    console.log('touchStart end ');
  },
  touchMove: function (e) {
    touchEndy = e.touches[0].pageY;
    var lindex = parseInt(touchEndy / rightheight * 27);
    var value = this.data.letters[lindex];
    var isShow = that.dialog.getDialogState();
    if('定位' != value){
      //不是点击定位,弹出触摸的字母
      this.showOrHideLetterDialog(isShow, value, false);
    }
    console.log(" touchMove touchEndy : " + touchEndy + " lindex : " + lindex + " value : " + value);
  },
  touchEnd: function (e) {
    var lindex = parseInt(touchEndy / rightheight * 27);
    var value = this.data.letters[lindex];
    var isShow = that.dialog.getDialogState();
    if ('定位' == value) {
    } else {
       //不是点击定位,弹出触摸的字母
      this.showOrHideLetterDialog(isShow, value, true);
    }
    console.log("touchEnd touchEndy : " + touchEndy + " lindex : " + lindex + " value : " + value);
  },
  showOrHideLetterDialog: function(isShow,letter,autodimss) {
    if (!isShow) {
      that.dialog.setLetter(letter);
      that.dialog.showDialog();
      this.startTime(autodimss);
    } else {
      clearTimeout(timer);
      this.startTime(autodimss);
      that.dialog.setLetter(letter);
    }
  }

上面的代码主要就是讲手指触摸的坐标转化成letters中的下标元素,然后通过类似dialog弹出来,虽然代码较多,但是认真看完,你会发现逻辑其实并不复杂,接下来看效果:
在这里插入图片描述
看到这里我们已经能正确的将手指触摸的坐标转换成对应字母弹出。

索引和列表联动

这个也比较简单,主要是通过scroll-view的scroll-into-view属性来定位。通过将letters中的元素映射成id,然后触摸和点击的时候动态改变scroll-view的scroll-into-view为触摸view的id就可以了。下面上代码:
完整wxml页面

// 完整页面wxml
<view class='content'>
  //增加scroll-into-view属性
  <scroll-view scroll-y='true' class='city-scroll' scroll-with-animation='true' scroll-into-view="{{toView}}">
    <view class='city-content'>
      //增加id用来做联动,定位特殊处理
      <view class='location-city-title' id='dw'>定位城市</view>
      <view class='location-parent'>
        <view class='location-city' bindtap='hotcity'>南昌市</view>
      </view>
      <view wx:for='{{citys}}' class='city-item' bindtap='selectcity' data-orgid='{{item.orgId}}' data-orgname='{{item.name}}'>
        //增加id用来做联动
        <text class='city-letter' wx:if='{{item.isshowletter}}' id='{{item.simplepinyin}}'>{{item.simplepinyin}}</text>
        <text class='city-name'>{{item.name}}</text>
      </view>
    </view>
  </scroll-view>
  <view class='right' bindtouchstart='touchStart' bindtouchmove='touchMove' bindtouchend='touchEnd' id='right'>
    <view wx:for="{{letters}}" class='letter' bindtap='letterclick' data-letter="{{item}}">{{item}}</view>
  </view>
  <dialog id='dialog'>
  </dialog>
</view>

完整js页面

// 完整js
// pages/citylist/citylist.js
var that;
var timer;
var items = require('../../data/citydata.js');
var rightheight = 0;
var touchEndy = 0;
Page({

  /**
   * 页面的初始数据
   */
  data: {
    letters: ['定位', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
    citys: [],
    toView: '',//用来做定位联动
  },
   
  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    that = this,
    that.setData({
      citys: items.citys
    })
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function () {
    //获得dialog组件
    this.dialog = this.selectComponent("#dialog");
  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    //创建节点选择器
    var query = wx.createSelectorQuery();
    query.select('#right').boundingClientRect()
    query.exec(function (res) {
      //res就是 所有标签为mjltest的元素的信息 的数组
      console.log(res);
      //取高度
      console.log("height : "+res[0].height);
      rightheight = res[0].height;
    })
  },
  //右侧索引表点击事件
  letterclick: function (e) {
    var letter = e.currentTarget.dataset.letter;
    var isShow = that.dialog.getDialogState();
    if('定位' == letter){//点击定位滚动到顶部
      that.setData({
        toView: 'dw',
      })
    }else{
      this.showOrHideLetterDialog(isShow,letter,true);
      that.setData({//定位到字母所在城市item
        toView: letter,
      })
    }
    console.log('letterclick letter : ' + letter);
  },
  startTime: function (autodimiss) {
    if (autodimiss){
      timer = setTimeout(function () {
        that.dialog.hideDialog();
      }, 1500)
    }
  },  //touch 事件有bug
  touchStart: function (e) {
    console.log('touchStart start ');
    touchEndy = e.touches[0].pageY;
    console.log('touchStart end ');
  },
  touchMove: function (e) {
    touchEndy = e.touches[0].pageY;
    var lindex = parseInt(touchEndy / rightheight * 27);
    var value = this.data.letters[lindex];
    var isShow = that.dialog.getDialogState();
    if('定位' != value){
      this.showOrHideLetterDialog(isShow, value, false);
    }
    console.log(" touchMove touchEndy : " + touchEndy + " lindex : " + lindex + " value : " + value);
  },
  touchEnd: function (e) {
    var lindex = parseInt(touchEndy / rightheight * 27);
    var value = this.data.letters[lindex];
    var isShow = that.dialog.getDialogState();
    if ('定位' == value) {
      that.setData({
        toView: 'dw',
      })
    } else {
      this.showOrHideLetterDialog(isShow, value, true);
      that.setData({
        toView: value,
      })
    }
    console.log("touchEnd touchEndy : " + touchEndy + " lindex : " + lindex + " value : " + value);
  },
  showOrHideLetterDialog: function(isShow,letter,autodimss) {
    if (!isShow) {
      that.dialog.setLetter(letter);
      that.dialog.showDialog();
      this.startTime(autodimss);
    } else {
      clearTimeout(timer);
      this.startTime(autodimss);
      that.dialog.setLetter(letter);
    }
  },
  selectcity: function(e) {
    var orgid = e.currentTarget.dataset.orgid
    var orgname = e.currentTarget.dataset.orgname
    wx.showToast({
      title: 'orgid : ' + orgid + ' orgname : ' + orgname,
      icon: 'none'
    })
  }
})
// 完整wxss
.content {
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  height: 100%;
}

.right {
  position: fixed;
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 60rpx;
  justify-content: space-around;
  align-items: center;
}

.letter {
  font-size: 20rpx;
  color: #1296DB;
  width: 60rpx;
  padding-top: 5rpx;
  padding-bottom: 5rpx;
  text-align: center;
}

.city-scroll {
  left: 0;
  position: fixed;
  height: 100%;
  width: 690rpx;
}

.city-content {
  display: flex;
  flex-direction: column;
}

.location-parent {
  display: flex;
  flex-direction: row;
}

.location-city-title {
  padding-left: 40rpx;
  padding-top: 30rpx;
  padding-bottom: 30rpx;
  background: rgba(223, 222, 222,0.5);
  color: gray;
  font-size: 30rpx;
}

.location-city {
  border: 1rpx solid #ccc;
  border-radius: 20rpx;
  margin-left: 40rpx;
  margin-top: 20rpx;
  padding-top: 15rpx;
  padding-bottom: 15rpx;
  font-size: 30rpx;
  width: 150rpx;
  text-align: center;
}

.city-item {
  display: flex;
  flex-direction: column;
}

.city-letter {
  font-size: 30rpx;
  padding-top: 30rpx;
  padding-left: 40rpx;
}

.city-name {
  border-bottom: 1px solid #ccc;
  background: rgba(223, 222, 222,0.5);
  padding-top: 20rpx;
  padding-bottom: 20rpx;
  font-size: 30rpx;
  color: gray;
  padding-left: 40rpx;
}

文章到这里,我们已经完整的实现了开始所展示的效果了。

尾巴

文章虽然有点长,但是逻辑并不是很复杂,关键地方都有加入注释,希望能对读者有所帮助。
如果文章中有错误的地方,欢迎大家留言指正。如果你喜欢我的文章,也欢迎给我点赞,评论,谢谢!

发布了32 篇原创文章 · 获赞 59 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/abs625/article/details/90212427