BalloonPicker
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiYWan5COwEmNjJzYjNTO1kzMwUjYhVmM2UDMzUzYzMTNxQzM38CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.gif)
preview
控件拆分:
元素 | 拆分 | 效果
-|-|-
基線 | 已選擇、未選擇 | 根據觸摸塊改變長度 |
觸摸塊 | 觸摸動畫、内外圓 | 觸摸時,内外圓按各自限制勻速放大;結束時,内外圓按各自限制勻速縮小 |
氣球 | 縮放、位移 | 觸摸前後縮放,移動時,勻速移動,中心旋轉 |
文本 | 描述、值回調 | 普通文本、觸摸顯示回調值 |
繪制流程拆分
繪制基線和觸摸塊
分别繪制選中和未選中的基線
然後繪制觸摸塊外圓和内圓
繪制基線和觸摸塊
為觸摸塊添加動畫
觸摸塊樣式動畫
以外圓圓心為中心,達到半徑100像素範圍内的觸摸點,均可觸發内圓縮放動畫。
1、預設觸摸狀态下,動畫以預設半徑開始勻速遞增,同時重新整理視圖,直到最大内圓半徑為止。
2、放大過程中,如果使用者手指離開螢幕,觸發MOVE_UP事件,則停止已有放大動畫,轉而執行縮小内圓半徑的動畫,直到達到最小内圓半徑為止。
3、如果縮小過程中使用者再次觸摸此區域,則重複執行過程1,以此達到跟随互動效果。
同理,外圓的縮放規則遵循上述規則
為了讓動畫效果更加平順,并且不浪費太多時間在縮放過程中,我們将在縮放開始前結束已在執行中的動畫,并重新計算剩餘縮放過程需要的時間差,用于目前縮放過程。
縮放動畫時間差計算公式:
實際動畫持續時間 = 動畫持續時間 * 剩餘縮(放)距離/總縮(放)距離
val remainingTime: Long = when {
this.increase -> (duration* (thumbInnerCircleRadiusMax - thumbInnerCircleRadiusTemp)/(thumbInnerCircleRadiusMax - thumbInnerCircleRadiusDefault)).toLong()
else -> (duration* (thumbInnerCircleRadiusTemp - thumbInnerCircleRadiusDefault)/(thumbInnerCircleRadiusMax - thumbInnerCircleRadiusDefault)).toLong()
}
觸摸效果
觸摸塊位移
當觸發MOVE_MOVE時,根據觸摸坐标x內插補點,更新觸摸塊的x坐标。同時,計算出此時選擇器的value,更新兩側基線狀态,并執行回調。
val x = pointOfThumbTemp.x + event.x - pointOfTouchDown.x
pointOfThumb = PointF( if (x > xOfTrackLayerEnd) xOfTrackLayerEnd else ( if (x < xOfTrackLayerStart) xOfTrackLayerStart else x), pointOfThumbTemp.y)
selectedValue = (this.minValue.toFloat() + (this.maxValue.toFloat() - this.minValue.toFloat()) * (pointOfThumb.x - xOfTrackLayerStart) / (widthOfView - 2 * xOfTrackLayerStart)).toLong()
postInvalidate()
listener?.callBack(selectedValue)
觸摸效果
氣球動畫
通過拆分:
1、ACTION_MOVE,當觸摸坐标發生位移,氣球旋轉對應角度以保持風力阻擋的慣性。氣球中心點到基線的垂線,與氣球中心點和觸點中心點 的直線的夾角,即為目前狀态下的旋轉角度
//分析圖
override fun locationOfThumb(pointF: PointF) {
pointThumb.set(pointF.x, height.toFloat() - trackLayer?.getPadding()!!)
val b = pointF.x - trackLayer?.getPadding()!!
val angleRoTan = -atan(b/distanceVerticalBetweenBalloonAndTrackLayer) / PI * 180
L("angleRoTan $angleRoTan")
balloon?.rotation = if (angleRoTan.toFloat() > 0F ) 0F else angleRoTan.toFloat()
postInvalidate()
}
旋轉氣球(假裝藍色塊是氣球)
此時氣球能跟着“線”被“手”帶”動“了,但是氣球還沒有移動,“線”也沒有無限長,行,我們先讓氣球移動起來。
這裡需要設定一個閥值,即“線”的長度,當超過這個閥值,則氣球将被“拽着”移動。
private fun moveBalloon(){
val ptb2 = (pointThumb.x - centerOfBalloon.x).toDouble().pow(2.0)
val c = sqrt (ptb2 + centerOfBalloon.y * centerOfBalloon.y)
...
}
直接計算“線”長來判斷,但是這樣需要繁瑣的符号運算,這裡我們可以直接找個參考資料,簡化邏輯過程:
private fun moveBalloon(){
val b = pointThumb.x - centerOfBalloon.x
...
}
通過觸點與氣球中心點的垂直距離的變化來判斷是否需要進行“拽着”移動:
override fun locationOfThumb(pointF: PointF) {
pointThumb.set(pointF.x, height.toFloat() - trackLayer?.getPadding()!!)
moveBalloon()
}
private fun moveBalloon(){
val b = pointThumb.x - centerOfBalloon.x
val ins = abs(b) - (height - pointThumb.y)
if (b != 0F && ins > 0){
val xOfBalloon = centerOfBalloon.x.toInt() - balloon?.layoutParams!!.width / 2 + if (b > 0) ins.toInt() else - ins.toInt()
balloon?.layout( xOfBalloon, balloon?.y!!.toInt(), xOfBalloon + balloon?.layoutParams!!.width, measuredHeight - trackLayer?.layoutParams!!.height)
centerOfBalloon.set(balloon?.x!! + balloon?.layoutParams!!.width / 2F, balloon?.y!! + balloon?.layoutParams!!.height / 2)
} else {
//TODO moveBalloonWithAnim
}
val angleRoTan = -atan(b/distanceVerticalBetweenBalloonAndTrackLayer) / PI * 180
L("angleRoTan $angleRoTan")
balloon?.rotation = angleRoTan.toFloat()
postInvalidate()
}
拖拽氣球
此時氣球已經可以被拽着走了,為了讓效果更加逼真,在閥值内,我們通過動畫來緩慢移動,
2、同時以勻速向新的圓點移動,直到氣球中心x與觸摸點x重合。
通過監聽将TrackLayer的touch資料傳遞給pickerView,改造了統一的接口:
interface TrackLayerListener {
fun layerTouchedDown()
fun layerTouchedUp()
fun layerTouchedMoving(value : Long, pointAtLayer : PointF)
}
在 layerTouchedMoving() 中處理氣球的移動邏輯.
當球心與圓點距離小于閥值時,中斷氣球動畫,直接布局氣球在picker中的位置;否則,執行新的氣球動畫:
override fun layerTouchedMoving(value: Long, pointAtLayer: PointF) {
pointThumb.set(pointAtLayer.x, height.toFloat() - trackLayer?.getPadding()!!)
val b = pointThumb.x.toInt() - centerOfBalloon.x.toInt()
if (abs(b) > distanceVerticalBetweenBalloonAndTrackLayer.toInt()){
initAnimation(ValueAnimator.ofInt(centerOfBalloon.x.toInt(), pointThumb.x.toInt()))
val xOfBalloon = (centerOfBalloon.x - balloon?.layoutParams!!.width / 2 + if (b > 0) b-distanceVerticalBetweenBalloonAndTrackLayer else b + distanceVerticalBetweenBalloonAndTrackLayer).toInt()
balloon?.layout( xOfBalloon , balloon?.y!!.toInt(), xOfBalloon + balloon?.layoutParams!!.width, balloon?.y!!.toInt() +balloon?.layoutParams!!.height )
centerOfBalloon.set(xOfBalloon + balloon?.layoutParams!!.width / 2F, balloon?.y!! + balloon?.layoutParams!!.height / 2)
rotateBalloon()
}
moveBalloon()
}
拖拽氣球
效果還可以,接下來就需要根據picker的取值來動态縮放氣球,同時維持住氣球的底部位置不變
本計劃直接調用scale API, 奈何privot也需要動态控制,不然不能維持氣球底部垂直位置不變。
override fun layerTouchedMoving(value: Long, pointAtLayer: PointF) {
//...
val valueAtBalloon =trackLayer?.minValue()!! + (trackLayer?.maxValue()!! - trackLayer?.minValue()!!) * centerOfBalloon.x/measuredWidth
val disScaleHeight = balloonHeightDefault * (valueAtBalloon - trackLayer?.minValue()!!) / (trackLayer?.maxValue()!! - trackLayer?.minValue()!!)
val disScaleWidth = balloonWidthDefault/2 * (valueAtBalloon - trackLayer?.minValue()!!) / (trackLayer?.maxValue()!! - trackLayer?.minValue()!!)
//...
balloon?.layout( xOfBalloon , balloonDefaultY.toInt() - disScaleHeight.toInt(), xOfBalloon + balloonWidthDefault.toInt() + disScaleWidth.toInt() * 2, (balloonDefaultY + balloonHeightDefault).toInt())
centerOfBalloon.set(xOfBalloon + disScaleWidth + balloonWidthDefault / 2F, balloonDefaultY + balloonHeightDefault/2 - disScaleHeight/2 )
//...
}
//...
}
給氣球打上輔助線,我們來看下效果:
拖拽氣球
4、氣球顯示隐藏
氣球能動能縮放了,接下來給氣球加入出入動畫,
預設情況下,不展示氣球,當ACTION_DOWN 觸發,氣球沖圓點漸顯 & 放大 & 移動 到初始位置;
當ACTION_UP 觸發,氣球從目前位置 淡出 & 縮小 & 移動 到圓點位置
override fun layerTouchedDown() {
balloon?.startAnimation(BalloonAnimSet.create(true, 0F, 0F, pointThumb.y - balloon?.y!!, 0F, context , listenerEnter))
}
override fun layerTouchedUp() {
balloon?.visibility = View.INVISIBLE
pointThumb.set(trackLayer?.centerPoint()!!.x, height.toFloat() - trackLayer?.getPadding()!!)
initAnimation(ValueAnimator.ofInt(centerOfBalloon.x.toInt(), pointThumb.x.toInt()))
moveBalloon()
balloon?.startAnimation(BalloonAnimSet.create(false, 0F, 0F, 0F, pointThumb.y - balloon?.y!!, context , listenerExit))
}
來看看效果吧:
拖拽氣球
開始使用
修飾一下,抛出必要的樣式設定方法,最終效果完成:
//使用方法
Add it in your root build.gradle at the end of repositories:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Step 2. Add the dependency
dependencies {
implementation 'com.github.fairytale110:BalloonPicker:1.0.1'
Then, Drop it to XML layout or new it
android:id="@+id/balloon_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
Finally, custom it's style as you want
fun load(){
balloon_picker.layerValues(10, 50, 5)
balloon_picker.defaultValue(30)
balloon_picker.setColorFoThumb("#FFFFFF".toColorInt(), "#512DA8".toColorInt())
balloon_picker.setColorForLayer("#512DA8".toColorInt(), "#BDBDBD".toColorInt())
balloon_picker.setColorForBalloon("#512DA8".toColorInt())
balloon_picker.setColorForBalloonValue("#FFFFFF".toColorInt())
balloon_picker.colorOfDesc = "#000000".toColorInt()
balloon_picker.colorOfValue = "#000000".toColorInt()
balloon_picker.desc = "Quantity"
balloon_picker.valueListener = object : BalloonPickerListener{
override fun changed(value: Long) {
Log.w("MainActivity","value: $value")
}
}
// val valueSelected = balloon_picker.getValue()
}
當然,這個控件還有很大的優化空間,歡迎諸位一起探讨。
THANKS