天天看點

自定義控件android onM,Android 自定義控件之 氣球選擇器

BalloonPicker

自定義控件android onM,Android 自定義控件之 氣球選擇器

preview

控件拆分:

元素 | 拆分 | 效果

-|-|-

基線 | 已選擇、未選擇 | 根據觸摸塊改變長度 |

觸摸塊 | 觸摸動畫、内外圓 | 觸摸時,内外圓按各自限制勻速放大;結束時,内外圓按各自限制勻速縮小 |

氣球 | 縮放、位移 | 觸摸前後縮放,移動時,勻速移動,中心旋轉 |

文本 | 描述、值回調 | 普通文本、觸摸顯示回調值 |

繪制流程拆分

繪制基線和觸摸塊

分别繪制選中和未選中的基線

然後繪制觸摸塊外圓和内圓

自定義控件android onM,Android 自定義控件之 氣球選擇器

繪制基線和觸摸塊

為觸摸塊添加動畫

觸摸塊樣式動畫

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

}

自定義控件android onM,Android 自定義控件之 氣球選擇器

觸摸效果

觸摸塊位移

當觸發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)

自定義控件android onM,Android 自定義控件之 氣球選擇器

觸摸效果

氣球動畫

通過拆分:

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()

}

自定義控件android onM,Android 自定義控件之 氣球選擇器

旋轉氣球(假裝藍色塊是氣球)

此時氣球能跟着“線”被“手”帶”動“了,但是氣球還沒有移動,“線”也沒有無限長,行,我們先讓氣球移動起來。

這裡需要設定一個閥值,即“線”的長度,當超過這個閥值,則氣球将被“拽着”移動。

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()

}

自定義控件android onM,Android 自定義控件之 氣球選擇器

拖拽氣球

此時氣球已經可以被拽着走了,為了讓效果更加逼真,在閥值内,我們通過動畫來緩慢移動,

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()

}

自定義控件android onM,Android 自定義控件之 氣球選擇器

拖拽氣球

效果還可以,接下來就需要根據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 )

//...

}

//...

}

給氣球打上輔助線,我們來看下效果:

自定義控件android onM,Android 自定義控件之 氣球選擇器

拖拽氣球

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))

}

來看看效果吧:

自定義控件android onM,Android 自定義控件之 氣球選擇器

拖拽氣球

開始使用

修飾一下,抛出必要的樣式設定方法,最終效果完成:

//使用方法

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