最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Android自定義view實現(xiàn)滾動選擇控件詳解

 更新時間:2022年11月02日 14:07:09   作者:撿一晌貪歡  
最近在開發(fā)中需要實現(xiàn)滾動進行類別的選擇,也就是我們所說的滾動選擇器,這里我們自定義來實現(xiàn)這個功能,這篇文章主要介紹了Android自定義view實現(xiàn)滾動選擇控件

前言

上篇文章通過一個有header和footer的滾動控件(Viewgroup)學了下MeasureSpec、onMeasure以及onLayout,接下來就用一個滾動選擇的控件(View)來學一下onDraw的使用,并且了解下在XML自定義控件參數(shù)。

需求

這里就是一個滾動選擇文字的控件,還是挺常見的,之前用別人的,現(xiàn)在選擇手撕一個,核心思想如下:

1、有三層不同大小及透明度的選項,選中項放在中間

2、接受一個列表的數(shù)據(jù),靜態(tài)時顯示三個值,滾動時顯示四個值

3、滑動會造成三個選項滾動,大小透明度發(fā)生變化,會有一個新的選項出現(xiàn)

4、滾動一定距離后,判定是否選中一個項目,并觸發(fā)動畫滾動到選定項 效果圖

編寫代碼

老實說下面寫的代碼并不好,特別是TextItem的繪制,本來是可以通過一個數(shù)學函數(shù),根據(jù)滑動距離來映射縮放比例及位置的,如有需要可以參考這個控件,忘了是幾年前從哪里抄的了,里面對文字的控制寫的很好。

下面是我手撕的代碼:

import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.View.MeasureSpec.*
import androidx.core.animation.addListener
import com.silencefly96.module_common.R
import kotlin.math.abs
import kotlin.math.min
/**
 * 滾動選擇文字控件
 * 核心思想
 * 1、有三層不同大小及透明度的選項,選中項放在中間
 * 2、接受一個列表的數(shù)據(jù),靜態(tài)時顯示三個值,滾動時顯示四個值
 * 3、滑動會造成三個選項滾動,大小透明度發(fā)生變化,會有一個新的選項出現(xiàn)
 * 4、滾動一定距離后,判定是否選中一個項目,并觸發(fā)動畫滾動到選定項
 */
class ScrollSelectView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0,
) : View(context, attributeSet, defStyleAttr){
    //默認字體透明度,大小不應該指定,應該根據(jù)view的高度按百分比適應
    companion object{
        const val DEFAULT_MAIN_TRANSPARENCY = 255
        const val DEFAULT_SECOND_TRANSPARENCY = (255 * 0.5f).toInt()
        //三種item類型
        const val ITEM_TYPE_MAIN = 1
        const val ITEM_TYPE_SECOND = 2
        const val ITEM_TYPE_NEW = 3
    }
    //兩層字體大小及透明度
    private var mainSize: Float = 0f
    private var secondSize: Float = 0f
    private val mainAlpha: Int
    private val secondAlpha: Int
    //主次item高度,由主item所占比例決定
    private val mainItemPercent: Float
    private var mainHeight: Float = 0f
    private var secondHeight: Float = 0f
    //字體相對于框的縮放比例
    private val textScanSize: Int
    //切換項目的y軸滑動距離門限值
    private var itemChangeYCapacity: Float
    //釋放滑動動畫效果間隔
    private var afterUpAnimatorPeriod: Int
    //數(shù)據(jù)
    @Suppress("MemberVisibilityCanBePrivate")
    var mData: List<String>? = null
    //選擇數(shù)據(jù)index
    @Suppress("MemberVisibilityCanBePrivate")
    var mCurrentIndex: Int = 0
    //繪制的item列表
    private var mItemList: MutableList<TextItem> = ArrayList()
    //單次事件序列累計滑動值
    private var mScrollY: Float = 0f
    //上次事件縱坐標
    private var mLastY: Float = 0f
    //畫筆
    private val mPaint: Paint
    init {
        //讀取XML參數(shù),設置相關屬性
        val attrArr = context.obtainStyledAttributes(attributeSet, R.styleable.ScrollSelectView)
        //三層字體透明度設置,未設置使用默認值
        mainAlpha = attrArr.getInteger(R.styleable.ScrollSelectView_mainAlpha,
            DEFAULT_MAIN_TRANSPARENCY)
        secondAlpha = attrArr.getInteger(R.styleable.ScrollSelectView_secondAlpha,
            DEFAULT_SECOND_TRANSPARENCY)
        textScanSize = attrArr.getInteger(R.styleable.ScrollSelectView_textScanSize, 2)
        //取到的值限定為dp值,需要轉換
        itemChangeYCapacity =
            attrArr.getDimension(R.styleable.ScrollSelectView_changeItemYCapacity, 0f)
        itemChangeYCapacity = dp2px(context, itemChangeYCapacity).toFloat()
        afterUpAnimatorPeriod =
            attrArr.getInteger(R.styleable.ScrollSelectView_afterUpAnimatorPeriod, 300)
        //獲取主item所占比例,在onMeasure中計算得到主次item高度
        mainItemPercent = attrArr.getFraction(
            R.styleable.ScrollSelectView_mainItemPercent, 1,1,0.5f)
        //回收
        attrArr.recycle()
        //設置畫筆,在構造中初始化,不要寫在onDraw里面,onDraw會不斷觸發(fā)
        mPaint = Paint().apply {
            flags = Paint.ANTI_ALIAS_FLAG
            style = Paint.Style.FILL
            //該方法即為設置基線上那個點究竟是left,center,還是right
            textAlign = Paint.Align.CENTER
            color = Color.BLACK
        }
        //創(chuàng)建四個TextItem
        mItemList.apply {
            add(TextItem(ITEM_TYPE_SECOND, true))
            add(TextItem(ITEM_TYPE_MAIN))
            add(TextItem(ITEM_TYPE_SECOND))
            add(TextItem(ITEM_TYPE_NEW))
        }
    }
    //設置控件的默認大小,實際viewgroup不需要
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //根據(jù)父容器給定大小,設置自身寬高,wrap_content需要用到默認值
        val width = getSizeFromMeasureSpec(300, widthMeasureSpec)
        val height = getSizeFromMeasureSpec(200, heightMeasureSpec)
        //Log.e("TAG", "onMeasure: height=$height")
        //設置測量寬高,一定要設置,不然錯給你看
        setMeasuredDimension(width, height)
        //如果滑動距離門限值沒有確定,應該根據(jù)view大小設定,默認高度的二分之一
        itemChangeYCapacity = if(itemChangeYCapacity == 0f) height / 2f
            else itemChangeYCapacity
        //有比例計算主次item高度
        mainHeight = height * mainItemPercent
        secondHeight = height * (1 - mainItemPercent) / 2
        //得到自身高度后,就可以按比例設置字體大小了
        mainSize = mainHeight / textScanSize
        secondSize = secondHeight / textScanSize
    }
    //根據(jù)MeasureSpec確定默認寬高,MeasureSpec限定了該view可用的大小
    private fun getSizeFromMeasureSpec(defaultSize: Int, measureSpec: Int): Int {
        //獲取MeasureSpec內模式和尺寸
        val mod = getMode(measureSpec)
        val size = getSize(measureSpec)
        return when (mod) {
            EXACTLY -> size
            AT_MOST -> min(defaultSize, size)
            else -> defaultSize //MeasureSpec.UNSPECIFIED
        }
    }
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //繪制顯示的text,最多同時顯示4個
        //Log.e("TAG", "onDraw: mScrollY=$mScrollY")
        for (item in mItemList) {
            item.draw(mScrollY / itemChangeYCapacity ,mPaint, canvas)
        }
    }
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        event?.let {
            when(event.action) {
                MotionEvent.ACTION_DOWN -> {
                    //按下開始計算滑動距離
                    mScrollY = 0f
                    mLastY = it.y
                }
                MotionEvent.ACTION_MOVE -> move(event)
                MotionEvent.ACTION_UP -> stopMove()
            }
        }
        //view是最末端了,應該攔截touch事件,不然事件序列將舍棄
        return true
    }
    private fun move(e: MotionEvent) {
        val dy = mLastY - e.y
        //更新mLastY值
        mLastY = e.y
        //設置滑動范圍,到達頂部不能下拉,到達底部不能上拉
        if (dy == 0f) return    //為什么會有兩次0???
        if (dy < 0 && mCurrentIndex - 1 == 0) return
        if (dy > 0 && mCurrentIndex + 1 == mData!!.size - 1) return
        //累加滑動距離
        mScrollY += dy
        //如果滑動距離切換了選中值,重繪前修改選中值
        if (mScrollY >= itemChangeYCapacity) changeItem(mCurrentIndex + 1)
        else if (mScrollY <= -itemChangeYCapacity) changeItem(mCurrentIndex - 1)
        //滑動后觸發(fā)重繪,繪制時處理滑動效果
        //Log.e("TAG", "move: mScrollY=$mScrollY")
        invalidate()
    }
    //統(tǒng)一修改mCurrentIndex,各個TextItem需要復位
    private fun changeItem(index: Int) {
        mCurrentIndex = index
        //消耗滑動距離
        mScrollY = 0f
        //對各個TextItem復位,重新調用setup即可
        for (item in mItemList) {
            item.setup()
        }
    }
    private fun stopMove() {
        //結束滑動后判定,滑動距離超過itemChangeYCapacity一半就切換了選中項
        val terminalScrollY: Float = when {
            mScrollY > itemChangeYCapacity / 2f -> itemChangeYCapacity
            mScrollY < -itemChangeYCapacity / 2f -> -itemChangeYCapacity
            //滑動沒有達到切換選中項效果,應該恢復到原先狀態(tài)
            else -> 0f
        }
        //這里使用ValueAnimator處理剩余的距離,模擬滑動到需要的位置
        val animator = ValueAnimator.ofFloat(mScrollY, terminalScrollY)
        //Log.e("TAG", "stopMove: mScrollY=$mScrollY, terminalScrollY=$terminalScrollY")
        animator.addUpdateListener { animation ->
            //Log.e("TAG", "stopMove: " + animation.animatedValue as Float)
            mScrollY = animation.animatedValue as Float
            invalidate()
        }
        //動畫結束時要更新選中的項目
        animator.addListener (onEnd = {
            if (mScrollY == itemChangeYCapacity) changeItem(mCurrentIndex + 1)
            else if (mScrollY == -itemChangeYCapacity) changeItem(mCurrentIndex - 1)
        })
        //滑動動畫總時間應該和距離有關
        val percent = terminalScrollY / (itemChangeYCapacity / 2f)
        animator.duration = (afterUpAnimatorPeriod * abs(percent)).toLong()
        //animator.duration = afterUpAnimatorPeriod.toLong()
        animator.start()
    }
    //單位轉換
    @Suppress("SameParameterValue")
    private fun dp2px(context: Context, dpVal: Float): Int {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, dpVal, context.resources
                .displayMetrics
        ).toInt()
    }
    //依賴于控件寬高及數(shù)據(jù),需要在onMeasure之后初始化一下
    inner class TextItem(
        private val type: Int,
        //上下兩個次item移動時是對稱的,要創(chuàng)建時確定
        private val isTopSecond: Boolean = false
    ){
        private var index: Int = 0
        private var x: Float = 0f
        private var y: Float = 0f
        private var textSize: Float = 0f
        private var alpha: Int = 0
        private var height: Float = 0f
        private var dSize = 0f
        private var dAlpha = 0
        private var dY = 0f
        private var isInit = false
        fun setup() {
            //x為中心即可
            x = measuredWidth / 2f
            //根據(jù)類型設置屬性
            when(type) {
                ITEM_TYPE_MAIN -> {
                    index = mCurrentIndex
                    y = measuredHeight / 2f
                    textSize = mainSize
                    alpha = mainAlpha
                    height = mainHeight
                }
                ITEM_TYPE_SECOND -> {
                    index = if (isTopSecond) mCurrentIndex - 1
                        else mCurrentIndex + 1
                    y = if (isTopSecond) secondHeight / 2f
                        else measuredHeight - secondHeight / 2f
                    textSize = secondSize
                    alpha = secondAlpha
                    height = secondHeight
                }
                else -> {
                    index = mCurrentIndex + 2
                    //初始化時未確定位置
                    y = 0F
                    textSize = 0f
                    alpha = 0
                    height = 0f
                }
            }
        }
        private fun calculate(delta: Float) {
            //根據(jù)類型得到變化值
            when(type) {
                ITEM_TYPE_MAIN -> {
                    //無論向那邊移動都應該是變小
                    dSize = (mainSize - secondSize) * -abs(delta)
                    dAlpha = ((mainAlpha - secondAlpha) * -abs(delta)).toInt()
                    //主次item中線之間的距離,delta>0頁面上移,y減小
                    dY = (mainHeight + secondHeight) / 2f * -delta
                }
                ITEM_TYPE_SECOND -> {
                    //以上面為準,下面對稱即可
                    if (isTopSecond && delta > 0 || !isTopSecond && delta < 0) {
                        //項目變?yōu)橄В底冃?
                        dSize = secondSize * -abs(delta)
                        dAlpha = (secondAlpha * -abs(delta)).toInt()
                        //消失的高度為次item的高度一半
                        //上面item消失時y減小,下面item消失時y增加
                        dY = secondHeight / 2f *
                                if (isTopSecond) -abs(delta) else abs(delta)
                    }else {
                        //項目變?yōu)檫x中,值變大
                        dSize = (mainSize - secondSize) * abs(delta)
                        dAlpha = ((mainAlpha - secondAlpha) * abs(delta)).toInt()
                        //上面item變?yōu)檫x中時y增加,下面item變?yōu)檫x中時y減小
                        dY = (mainHeight + secondHeight) / 2f *
                                if (isTopSecond) abs(delta) else -abs(delta)
                    }
                }
                else -> {
                    //新項目終態(tài)就是次item,無論怎么移動值都是變大的
                    dSize = secondSize * abs(delta)
                    dAlpha = (secondAlpha * abs(delta)).toInt()
                    //從邊沿移動到次item位置,即次item高度一半
                    //delta>0從下面出現(xiàn),y應該變小,delta<0從上面出現(xiàn),y變大
                    dY = secondHeight / 2f * -delta
                    //移動時才確定新item的y和index
                    y = if (delta > 0) measuredHeight.toFloat() else 0f
                    index = if (delta > 0) mCurrentIndex + 2 else mCurrentIndex - 2
                }
            }
        }
        fun draw(delta: Float, paint: Paint, canvas: Canvas?) {
            //確保在onMeasure后初始化
            if (!isInit) {
                setup()
                isInit = true
            }
            //計算屬性變化
            calculate(delta)
            //修改畫筆并繪制,注意變化的值都是相對于原來的值,不要去修改原來的值
            paint.textSize = textSize + dSize
            paint.alpha = alpha + dAlpha
            canvas?.drawText(getText(), x, getBaseline(paint, y + dY), paint)
        }
        private fun getText(): String {
            //判定范圍
            if (index < 0 || index >= mData!!.size) return ""
            return mData!![index]
        }
        private fun getBaseline(paint: Paint, tempY: Float): Float {
            //繪制字體的參數(shù),受字體大小樣式影響
            val fmi = paint.fontMetricsInt
            //top為基線到字體上邊框的距離(負數(shù)),bottom為基線到字體下邊框的距離(正數(shù))
            //基線中間點的y軸計算公式,即中心點加上字體高度的一半,基線中間點x就是中心點x
            return tempY - (fmi.top + fmi.bottom) / 2f
        }
    }
}

主要問題

自定義XML參數(shù)

這個網(wǎng)上應該有很多教程了,主要就是要創(chuàng)建一個value里面的xml來定義屬性,在XML使用的使用引入命名空間,并使用這些屬性,最后在控件代碼中讀取參數(shù)值。下面是這個控件的自定義屬性:

res->value->scrolll_select_view_style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ScrollSelectView">
        <attr name="mainAlpha" format="integer"/>
        <attr name="secondAlpha" format="integer"/>
        <attr name="textScanSize" format="integer"/>
        <attr name="changeItemYCapacity" format="dimension"/>
        <attr name="afterUpAnimatorPeriod" format="integer"/>
        <attr name="mainItemPercent" format="fraction"/>
    </declare-styleable>
</resources>

這里有六個控制屬性,mainAlpha和secondAlpha是設定中間和第二層文字的透明度,值為[0,255];textScanSize是文字相對于文字框的縮放比例,比如設置為2的話文字大小就是文字框的一半;changeItemYCapacity是滑動導致item切換的距離,單位為dp,即手指滑過這么長的距離就切換了選中項;afterUpAnimatorPeriod是手指抬起時,恢復到默認狀態(tài)或者跳轉到指定狀態(tài)的最大時間間隔,動畫時間會根據(jù)滑動距離占changeItemYCapacity的比例計算得到;mainItemPercent是中間item占整個控件的高度比例,大小為[0,100],第二層的item的高度為剩下高度的一半。

Paint的初始化

Paint的初始化也是一個老生常談的問題了,不能在onDraw里面創(chuàng)建,因為onDraw會頻繁被調用,同類對象也不應該在onDraw里面創(chuàng)建。

控件的默認大小

自定義view需要在onMeasure設定默認大小,不然使用wrap_content的時候會出問題,前幾篇文章和注釋都寫的很清楚了。

和寬高有關的默認屬性設置

所有和控件寬高有關的默認屬性都需要在onMeasure中設置,另外TextItem的setup中用到了measuredWidth和measuredHeight,也需要在onMeasure后初始化,這里就在第一次繪制的時候初始化了。

文字繪制到中心位置

通過paint要將文字繪制到中心位置,需要結合textAlign(Paint.Align.CENTER)和fontMetricsInt計算得到繪制的縱坐標位置。

滑動邏輯處理

因為這個繼承的是View,所以在onTouchEvent中消耗事件即可,一定要對ACTION_DOWN事件返回true。

移動時做了三步操作,一是判定是否能夠移動,二是累加移動dy并觸發(fā)重繪,三是判斷滑動距離是否切換了選中值,達到切換條件時,應該修改選中的index、累加的滑動距離,并將各個item重置到原來的位置及狀態(tài)(ps.好像就是改了個index。。。)。這里有點不太好理解,就是每當滑動達到切換條件后,就修改選中,重置各個item,邏輯上是一個跳躍性修改,但是對于draw來說,只是繪制的新的圖像罷了。

滑動結束后觸發(fā)動畫滾動到選定項

當滑動結束后應該將控件滑動到一個選中項,即讓選中項放置到中間,有三種情況,一個是沒有切換,一個是上一個index,一個是后一個index。這里利用了ValueAnimator去模擬滑動,首先計算到最終狀態(tài)的滑動距離,然后從當前滑動距離不斷更新,最后到達最終狀態(tài)的滑動距離。只要更新累加的滑動值,觸發(fā)重繪,在onDraw中會和move一樣進行處理。最后在動畫結束后,要和move一樣切換選中值及一些其他操作。

滑動導致的文字的繪制

文字的繪制我封裝到了TextItem這個內部類里,還是比較復雜,可能有三層item吧,不應該搞太多的。

繪制邏輯主要分成兩部分,一部分是靜態(tài)的位置及狀態(tài),一部分是滑動導致的變化值計算。靜態(tài)的位置及狀態(tài)寫在setup里面,還是比較簡單的,就是根據(jù)type去確定。

滑動導致的變化值計算就復雜多了,主要就是根據(jù)滑動距離和滑動切換項目的門限值確定一個百分比delta,再根據(jù)delta去計算變換的屬性值。實際上想簡單點,不就是屬性變大或者屬性變小的問題么,首先根據(jù)delta的正負值確定滑動的方向,再通過滑動方向去確定該類型的item應該屬性變大或者屬性變小,一個一個去設置就行了,就是代碼比較多而已。

寫到這里我發(fā)現(xiàn)前面我說應該用一個數(shù)學函數(shù)去映射的,我這abs(delta)不就是一個映射函數(shù)么。。果然寫代碼和寫文章更能讓直接深刻理解內容。這里abs(delta)換成拋物線確實可能會好看些,有機會改改。

使用

使用起來很簡單,直接設定數(shù)據(jù)和初始index就可以,暫時沒有動態(tài)切換,可以重寫下setter函數(shù),invalidate()就可以了。

xml

    <com.silencefly96.module_common.view.ScrollSelectView
        android:id="@+id/hhView"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="@color/teal_700"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

code

        binding.hhView.mData = ArrayList<String>().apply{
            add("第一個")
            add("第二個")
            add("第三個")
            add("第四個")
            add("第五個")
        }
        binding.hhView.mCurrentIndex = 2

范圍限定偶爾不生效

實際運行起來我發(fā)現(xiàn)對范圍限定偶爾會不生效,暫時還沒有解決,應該是動畫沒有完成就滑動造成的,不太好辦,當然這個控件主要目的是學習onDraw,寫的這么復雜了,目的已經(jīng)達到了,要學習繪制的話后面再寫幾個控件吧。

到此這篇關于Android自定義view實現(xiàn)滾動選擇控件詳解的文章就介紹到這了,更多相關Android滾動選擇內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論

柞水县| 孝昌县| 柳州市| 涪陵区| 棋牌| 余干县| 银川市| 育儿| 大兴区| 无极县| 九台市| 开原市| 齐齐哈尔市| 马山县| 林州市| 湖南省| 樟树市| 双辽市| 西峡县| 东台市| 和龙市| 岢岚县| 甘南县| 九龙坡区| 全椒县| 营口市| 镇远县| 翁牛特旗| 略阳县| 长治市| 田东县| 汽车| 绵竹市| 铜山县| 文登市| 科尔| 三河市| 辉县市| 丽水市| 渭南市| 乡城县|