picker ui 組件
首先我們需要思考規(guī)范組件的使用、提供那些可選參數(shù)授嘀、可響應(yīng)哪些事件演侯、以及提供哪些方法第租。
下面使用picker組件為例膀息,來(lái)一步一步的完成自己的ui組件般眉。
項(xiàng)目使用vue框架、按照同樣的思路當(dāng)然也可以切換到react潜支、angular
首先我們來(lái)思考picker組件的使用場(chǎng)景甸赃,常見(jiàn)的有城市選擇、多列聯(lián)動(dòng)選擇等場(chǎng)景冗酿。常常是用戶點(diǎn)擊一個(gè)按鈕彈出背景框和picker組埠对。初始化相關(guān)樣式、數(shù)據(jù)后裁替,用戶操作pickerItem项玛,滑動(dòng)選擇數(shù)據(jù),選定后確認(rèn)或者取消事件弱判。我們可以看到大致就是這樣一個(gè)過(guò)程襟沮。
下面我們來(lái)一步一步的完成這個(gè)組件,最后并不斷完善昌腰。
使用
1开伏、每次picker被用戶改變值的時(shí)候change事件
2、組件接受的參數(shù)數(shù)組 colcumns 遭商,數(shù)組可以簡(jiǎn)單的一元數(shù)組/也可能成員為對(duì)象的數(shù)組
3固灵、有哪些可自定義的參數(shù)? 如確定/取消/中間標(biāo)題/是否顯示toolbar等
哪些我們就先簡(jiǎn)單的列出組件的使用
<picker
@change="onChange()"
:columns="columns"
:showToolbar="true"
:title="title">
</picker>
組件html結(jié)構(gòu)
先從最簡(jiǎn)單的開(kāi)始我們需要一個(gè)picker展示的數(shù)據(jù)
先寫組件的html結(jié)構(gòu),da-picker 是最外層劫流,da-picker__toolbar是picker頂部的確定/取消按鈕巫玻,da-column是picker外層里面包裹了pickerItem的數(shù)據(jù)項(xiàng)。da-iframe是picker選中的橫條祠汇,固定位置的上下1px的橫條大审。
<template>
<div class="da-picker">
<div class="da-picker__toolbar">
</div>
<div class="da-picker__column" :style="calcHeight">
<div>
<ul>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
</div>
<div class="da-iframe da-1pxborder--top-bottom"></div>
</div>
</div>
</template>
樣式
在寫一個(gè)ui組件的時(shí)候,樣式庫(kù)同樣重要座哩。這里先不對(duì)樣式庫(kù)進(jìn)行處理徒扶,只是先將和picker組件相關(guān)的樣式放在同一組件之內(nèi)。
<style lang="scss">
.da-picker{
overflow: hidden;
position: relative;
.da-picker__toolbar{
display: flex;
justify-content: space-between;
}
.da-column{
position: relative;
overflow: hidden;
}
.da-iframe{
left: 0;
top: 0;
width: 100%;
position: absolute;
transform: translateY(-50%);
border: 1px 0;
}
}
// 公共樣式需要抽離出來(lái)
[class*='da-1pxborder']::after{
content: '';
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
transform: scale(.5);
pointer-event: none;
border: 0px solid #e5e5e5;
}
<style>
寫完了頁(yè)面結(jié)構(gòu)和css后我們著重來(lái)思考整個(gè)組件的需要處理哪些事情根穷。
數(shù)據(jù)初始化
<script>
export default {
name: 'picker',
props: {
columns: {
type: Array,
default: () => []
},
count: {
type: Number,
default: 5
}
},
data () {
return {
isSimple: false
}
},
computed: {
isSimple () {
return this.columns.length && !this.columns[0].values
}
}
created () {
this.initColumn()
},
methods: {
initColumn () {
// 數(shù)據(jù)類型判斷初始化結(jié)構(gòu)數(shù)據(jù)
const column = this.isSimple ? [{'values': this.columns}] : columns
}
}
}
</script>
樣式初始化
computed: {
calcHeight () {
return {
height: (this.count - 1) * this.itemHeight // props itemHeight
}
}
}
初始化樣式后將高度添加到da_columns 上姜骡。
下面我們將pickerItem渲染的內(nèi)容抽象出來(lái)為單獨(dú)的組件。這時(shí)我們修改picker的組件為
<template>
<div class="da-picker">
<div class="da-picker__toolbar">
</div>
<div class="da-picker__column" :style="calcHeight">
<pickItem :column="column"></pickItem>
<div class="da-iframe da-1pxborder--top-bottom"></div>
</div>
</div>
</template>
<template>
<div>
<ul>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
</div>
</template>
為上面的html添加上數(shù)據(jù)屿良、以及需要的touch相關(guān)事件.
<template>
<div>
<ul
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<li v-for="item in options" :key="item">{{item}}</li>
</ul>
</div>
</template>
狀態(tài)改變
其中最核心的就是每次pickerItem的改變圈澈。我們給每一個(gè)Item一個(gè)index,setIndex(index, userAction) 為核心狀態(tài)改變函數(shù)尘惧,userAction 為用戶手動(dòng)點(diǎn)擊觸摸選擇改變,為用戶觸發(fā)一個(gè)change事件康栈。
setIndex (index, userAction) {
this.offset = - index * this.itemHeight
if(index !== this.currentIndex) {
userAction && this.$emit('change', index)
}
}
這樣我們得到了picker最核心的一個(gè)方法。下面我們來(lái)分析每一次index改變時(shí)候需要進(jìn)行的樣式位置計(jì)算。
- 初始化時(shí)候ul元素會(huì)有一個(gè)初始化baseOffset啥么。隨后每次index的改變ui的offset都為 offset + baseOffset登舞。
- 那么重點(diǎn)就是如何計(jì)算offset。
- 我們每次操作的時(shí)候touchStart 需要記錄當(dāng)前的 firstOffset
- 然后是touchMove移動(dòng)的deltaY 此時(shí)offset = firstOffset + deltaY
- 最后touchEnd結(jié)束的時(shí)候 通過(guò)offset的值計(jì)算出index悬荣,最后調(diào)用setIndex(index, userAction)
用戶操作
onTouchStart (event) {
this.startY = event.touches[0].clientY
this.firstOffset = this.offset
this.duration = 0
},
onTouchMove (event) {
const deltaY = event.touches[0].clientY - this.startY
this.offset = range(this.firstOffset + deltaY, [- this.itemHeight * this.count, this.itemHeight ]) // 最大offset 范圍需要注意菠秒,ul 高度為 (this.count - 1) * this.itemHeight ,由于在前面已經(jīng)對(duì)位置進(jìn)行了初始化,ul上下可偏移的位置為自身位置加上一個(gè)itemHeight
},
onTouchEnd (event) {
this.duration = 200
const index = rang(Math.round(this.offset / this.itemHeight), [0, this.count - 1])
this.setIndex(index, true)
}
通過(guò)touchStart獲取初始化的位置氯迂,以及當(dāng)前firstOffset践叠,touchMove的時(shí)候計(jì)算this.offset的位置,并且限制最大滑動(dòng)范圍嚼蚀。最后通過(guò)touchEnd 計(jì)算出滑動(dòng)后的Index禁灼,調(diào)用setIndex以及duration時(shí)間完成切換
未完