優雅的氣球選擇器 BalloonPicker

[復制鏈接]
來自: fairytale110 分類: Android精品源碼 上傳時間: 2019-10-24 16:48:47
Tag:

項目介紹:

BalloonPicker

Download demo apk

控件拆分:

元素 拆分 效果
基線 已選擇、未選擇 根據觸摸塊改變長度
觸摸塊 觸摸動畫、內外圓 觸摸時,內外圓按各自約束勻速放大;結束時,內外圓按各自約束勻速縮小
氣球 縮放、位移 觸摸前后縮放,移動時,勻速移動,中心旋轉
文本 描述、值回調 普通文本、觸摸顯示回調值

繪制流程拆分

繪制基線和觸摸塊

分別繪制選中和未選中的基線
然后繪制觸摸塊外圓和內圓 

為觸摸塊添加動畫

觸摸塊樣式動畫

以外圓圓心為中心,達到半徑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

<tech.nicesky.balloonpicker.BalloonPickerView
        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()
 }

當然,這個控件還有很大的優化空間,歡迎諸位一起探討。歡迎star

GitHub: https://github.com/fairytale110/BalloonPicker

相關源碼推薦:

我來說兩句
所有評論(17)
打個醬油的 2019-10-24 18:46:24
感謝分享,安卓巴士有你更精彩:lol
回復
ff12345 2019-10-24 18:46:44
感謝分享,樓主V5~
回復
bug是啥 2019-10-24 18:50:54
幫幫頂頂!!
回復
無限釋囚 2019-10-24 19:03:44
每次我都積極回帖的,想要安幣~
回復
仲夏炎涼。 2019-10-24 19:05:04
不錯不錯,樓主辛苦了。。。
回復
subsoil 2019-10-24 19:10:14
樓主威武,以后多發干貨,多辦活動~!
回復
apkbus熱心網友 2019-10-25 10:38:47
我只是路過打醬油的。
回復
123下一頁
提取碼:  下載次數:3 狀態:已購或VIP 售價:10(原價:10)金錢 下載權限:初級碼農 
531 0 3
代碼貢獻英雄榜
用戶名 下載數
聯系我們
首頁/微信公眾賬號投稿
帖子代碼編輯/版權問題
QQ:435399051,1294855032
如何獲得代碼達人稱號?
如何成為簽約作者?
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

龙江福彩p62开奖 钢琴老师赚钱多 代理oppo手机赚钱吗 街机捕鱼怎么赢金币 河南麻将怎么打的 平台交押金可以赚钱吗 全游娱乐赚钱方式 麻将平台代理怎样注册 60年代制皮产品卖商店赚钱吗 15岁可不可以赚钱 玩彩网游戏 搞激光焊赚钱吗 开太极拳馆能赚钱吗 彩38彩票安卓 微信小程序成语赚钱乐能提现吗 摆地摊卖凉菜和卤肉赚钱吗 扫码点单赚钱吗