技術概述
需求是實現一個包含三種狀態,分別是底部、半屏、全屏的浮層,並且浮層支持用戶隨意拖動,同時還要防止用戶過度拖動。技術難點在於對動畫的控制,必須得有一定的過渡動畫否則整體會很突兀。
技術詳述
首先看一下預期的樣子,圖為ios自帶高德地圖的底部浮層。
最開始的動畫我是用CSS的keyframe來寫的,即利用關鍵幀來標記三個狀態,將過渡時間設置為300毫秒,出現的問題就是在程序運行時控制動畫只能通過動態增刪類來實現,十分繁瑣。這樣可以實現簡單的點擊事件切換狀態,但是無法讓用戶滑動。
這時發現了uni-app官方的動畫插件uni-transition,閱讀文檔之后發現類似於關鍵幀,就是從一個狀態到另一個狀態的滑動,設置好動畫和過渡時間,可以很方便地用js來控制。
<uni-transition custom-class="location-box" :show="showLocationBox" ref="locationBox">
</uni-transition>
上方代碼為uni-transition的使用方式。custom-class用於綁定自己寫的類,:show用於綁定浮層的顯示,ref用戶注冊動畫的引用信息,可以讓我們用this來控制。
結合官方文檔,來談談uni-transition的使用方式。
this.$refs.locationBox.init({
duration: 1000,
timingFunction: 'linear',
transformOrigin: '50% 50%',
delay: 500
})
使用init方式可以給動畫初始化,並覆蓋掉原本的動畫參數,這里我們用不到,但是必須要知道可以用init方法覆蓋原本的動畫。
先從狀態切換開始實現,原理就是用uni-transition標簽包裹浮層內容,使用fixed位置,動畫的參數就是修改其height和top的內容,輔以uni-transition動畫的300毫秒過渡時間,以達到平滑過渡的效果。
//step方式相當於設置一個關鍵幀
this.$refs.locationBox.step({
//設置要執行到的樣式大小,當調用run方法之后,uni-transition就會從當前的height和top以300毫秒過渡變化到
//height=14vh,top=86vh的狀態
height: '14vh',
top: '86vh'
}, {
duration: 300,
})
//run方法執行動畫
this.$refs.locationBox.run(() => {
//這是run方法的回調函數,可以在這里執行動畫完成之后的操作
})
簡單來說就是將三個狀態封裝為三個函數,每次要狀態變化時就調用對應函數,以此能實現基本的綁定事件進行動畫過渡。
為什么不封裝成一個函數通過參數改變內容?因為狀態的變化同時包含了一些其他狀態的改變需要在函數中一起動態變化,這里可以自主選擇。
這樣就可以實現綁定事件的效果,如下圖,通過綁定浮層底部的橫條的點擊時間和input的聚焦事件,來控制動畫的執行。
接下來就是要讓用戶可以利用黑色橫條拖動整個框的移動,因為網上找到寫可拖動底部浮層的教程都是寫Android的,這里我並沒有找到比較好的辦法,只能用監聽事件來實現。
利用將@touchend和@touchmove兩個事件綁定到黑色橫條上,監聽用戶對於黑色橫條的移動。
@touchmove:移動時觸發
@touchend:移動結束時觸發
<view @click="showList" @touchmove="touchLocationBox" @touchend="locationBoxReset">
<!-- 這里是黑色橫條的內容或樣式 -->
</view>
然后再來看兩個事件綁定函數
///監聽觸摸滑動事件
touchLocationBox(res) {
//利用監聽事件獲取當前用戶點擊屏幕的位置
//而后將其除於整個屏幕的高度獲取top值
let top = res.changedTouches[0].pageY / this.windowHeight
//防止頂部過度拖拽
//當拖拽部分小於10%時禁止用戶繼續拖拽
if (top < 0.1) {top = 0.1}
//防止底部過度拖拽
//同理,禁止拖拽到85%以上
else if (top > 0.85) {top = 0.85}
//這里是動畫執行的關鍵
//每次函數執行時就執行一個過渡時間為0的動畫
//動畫內容就是將當前的heiht值和top值過渡到用戶手指拖拽到的值
this.$refs.locationBox.step({
top: top * 100 + 'vh',
height: (1 - top) * 100 + 'vh',
}, {
duration: 0,
})
this.$refs.locationBox.run(() => {
})
//維護一個當前的top值,動態改變
this.locationBoxTop = top
},
這里要修改前面的動態函數。
this.$refs.locationBox.step({
height: '14vh',
top: '86vh'
}, {
duration: 300,
})
this.$refs.locationBox.run(() => {
//動畫執行完畢后更新當前的top值
this.locationBoxTop = 0.86
})
因為需要強制修改top值,所以uni-transition需要新增綁定top參數,並使用!important強制更新。
<uni-transition custom-class="location-box" :show="showLocationBox" ref="locationBox"
:styles="{top:locationBoxTop*100+'vh !important',height:(1-locationBoxTop)*100+'vh !important'}">
</uni-transition>
這里已經基本實現了用戶拖拽的問題,但是我們需要的僅僅為3個狀態,即底欄、半屏、全屏三個狀態,到這里用戶拖動后浮層會停在當前位置。這里就需要使用到@touchend事件,在用戶移動結束時觸發函數,我們在這里對浮層進行復位。
//拖動結束將地點框復位
locationBoxReset(res) {
//獲取當前的top值
let top = this.locationBoxTop
//自主設定拖動閾值,
if (top < 0.4) {
//這里轉化為全屏狀態
} else if (top > 0.7) {
//這里轉化為底欄狀態
} else {
//這里轉化為半屏狀態
}
},
然后來看一下最終效果,用戶可以點擊橫條實現動畫,還可以通過橫條拖動浮層,並且不會出現拖拽過渡的現象,當用戶拖拽到一定閾值時松手,浮層會自動復位到附近的狀態。
問題和解決
在動畫實現的問題還是遇到了很多問題。先拋開實現的問題不談,目前已經存在的問題就是拖拽時的幀率不夠高,因為一直在執行動畫所以效率不夠高,我個人覺得在小程序上是夠用的,雖然不能想高德地圖那樣流暢,但也並不會感到卡頓。這個問題我並沒有解決,這僅僅是我個人的實現方式,如果看到博客的人有效率更高的動畫方式歡迎在評論區指教。
然后說說實現上的問題。最開始的問題就是浮層在執行一次動畫之后會將step方法內的參數直接加在整個浮層樣式的尾部,而不是修改他的值,這是uni-transition插件源碼的問題,如下圖底部的height和top就是動畫執行后添加的樣式,而CSS的寫在后面的樣式會覆蓋掉寫在前面的樣式,所以我們動態綁定的height和top就無法生效,解決的方式就是在同態綁定時加上!important,給綁定的樣式最高的權重,使其不會被覆蓋。

上面那個問題我真的找了好久才找到,最后是通過看wxml的代碼變化才找到。所以這個可以提個醒,寫這種動畫可以看看wxml的代碼變化,注意樣式的覆蓋。
為了函數調用起來更加方便,我將三個狀態封裝為一個change方法,在使用使只需要使用change(0)可以直接執行到底欄狀態。
//更改浮層狀態,0:底欄(默認);1:半屏;2:全屏;
change(status) {
if (status === 0) {
//執行動畫和其他參數的改變
} else if (status === 1) {
//執行動畫和其他參數的改變
} else if (status === 2) {
//執行動畫和其他參數的改變
}
},
最后說一個與動畫無關但很有可能會遇到的問題,如果在浮層內有搜索框,在動畫移動時可能會出現input內的placeholder移動殘影的問題,即input中的內容總是慢動畫一步移動,極大地影響了用戶體驗。我解決的方式就是在動畫執行時將input框禁用掉,即綁定其disabled屬性,只要input是出於禁用的狀態,就不會出現移動殘影的問題。
總結
uni-transition插件由於其比較小眾,用的人比較少,所以網上能找到的教程並不多,建議最優先摸清官方的文檔。基於這種過渡動畫的方式,可以做出很多復雜的動畫。