亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android實(shí)現(xiàn)自動(dòng)變換大小的ViewPager

 更新時(shí)間:2022年11月02日 11:20:14   作者:撿一晌貪歡  
ViewPager使用適配器類將數(shù)據(jù)和view的處理分離,ViewPager的適配器叫PagerAdapter,這是一個(gè)抽象類,不能實(shí)例化,所以它有兩個(gè)子類:FragmentPagerAdapter 和 FragmentStatePagerAdapter,這兩個(gè)都是處理頁(yè)面為Fragment的情況

前言

上一篇做了一個(gè)滑動(dòng)折疊的Header控件,主要就是練習(xí)了一下滑動(dòng)事件沖突的問(wèn)題,控件和文章寫(xiě)的都不怎么樣。本來(lái)想通過(guò)這篇文章的控件,整合一下前面六篇文章的內(nèi)容的,結(jié)果寫(xiě)的太復(fù)雜了,就算了,沒(méi)有新的技術(shù)知識(shí),功能也和之前的安卓廣東選擇控件類似,不過(guò)在寫(xiě)的過(guò)程還是有點(diǎn)難度的,用來(lái)熟悉自定義view知識(shí)還是很不錯(cuò)的。

需求

這里我也不知道應(yīng)該怎么描述這個(gè)控件,標(biāo)題里用的大小自動(dòng)變換的類ViewPager,一開(kāi)始我把它叫做模仿桌面切換的多頁(yè)面切換控件。大致就是和電視那種切換頁(yè)面時(shí),中間頁(yè)面大,邊上頁(yè)面小,切換到中間會(huì)有變大的動(dòng)畫(huà)效果,我是覺(jué)得這樣的控件和炫酷。

核心思想如下:

1、類似viewpager,但同時(shí)顯示兩種頁(yè)面,中間為主頁(yè)面,左右為小頁(yè)面,小頁(yè)面大小一樣,間距排列

2、左右滑動(dòng)可以將切換頁(yè)面,超過(guò)頁(yè)面數(shù)量大小不能滑動(dòng),滑動(dòng)停止主界面能自動(dòng)移動(dòng)到目標(biāo)位置

效果圖

編寫(xiě)代碼

這里代碼寫(xiě)的還是挺簡(jiǎn)單的,沒(méi)有用到ViewPager那樣的Adapter,也沒(méi)有處理預(yù)加載問(wèn)題,滑動(dòng)起來(lái)不是特別流暢,頁(yè)面放置到頂層時(shí)切換很突兀,但是還是達(dá)到了一開(kāi)始的設(shè)計(jì)要求吧!

import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import androidx.core.animation.addListener
import androidx.core.view.children
import com.silencefly96.module_common.R
import java.util.*
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.roundToInt
/**
 * @author silence
 * @date 2022-10-20
 */
class DesktopLayerLayout @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ViewGroup(context, attributeSet, defStyleAttr) {
    companion object{
        // 方向
        const val ORIENTATION_VERTICAL = 0
        const val ORIENTATION_HORIZONTAL = 1
        // 狀態(tài)
        const val SCROLL_STATE_IDLE = 0
        const val SCROLL_STATE_DRAGGING = 1
        const val SCROLL_STATE_SETTLING = 2
        // 默認(rèn)padding值
        const val DEFAULT_PADDING_VALUE = 50
        // 豎向默認(rèn)主界面比例
        const val DEFAULT_MAIN_PERCENT_VERTICAL = 0.8f
        // 橫向默認(rèn)主界面比例
        const val DEFAULT_MAIN_PERCENT_HORIZONTAL = 0.6f
        // 其他頁(yè)面相對(duì)主界面頁(yè)面最小的縮小比例
        const val DEFAULT_OTHER_VIEW_SCAN_SIZE = 0.5f
    }
    /**
     * 當(dāng)前主頁(yè)面的index
     */
    @Suppress("MemberVisibilityCanBePrivate")
    var curIndex = 0
    // 由于將view提高層級(jí)會(huì)搞亂順序,需要記錄原始位置信息
    private var mInitViews = ArrayList<View>()
    // view之間的間距
    private var mGateLength = 0
    // 滑動(dòng)距離
    private var mDxLen = 0f
    // 系統(tǒng)最小移動(dòng)距離
    private val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
    // 控件狀態(tài)
    private var mState = SCROLL_STATE_IDLE
    // 當(dāng)前設(shè)置的屬性動(dòng)畫(huà)
    private var mValueAnimator: ValueAnimator? = null
    // 實(shí)際布局的左右坐標(biāo)值
    private var mRealLeft = 0
    private var mRealRight = 0
    // 上一次按下的橫豎坐標(biāo)
    private var mLastX = 0f
    // 方向,從XML內(nèi)獲得
    private var mOrientation: Int
    // 是否對(duì)屏幕方向自適應(yīng),從XML內(nèi)獲得
    private val isAutoFitOrientation: Boolean
    // padding,從XML內(nèi)獲得,如果左右移動(dòng),則上下要有padding,但左右沒(méi)有padding
    private val mPaddingValue: Int
    // 豎向主內(nèi)容比例,從XML內(nèi)獲得,剩余兩邊平分
    private val mMainPercentVertical: Float
    // 橫向主內(nèi)容比例,從XML內(nèi)獲得,剩余兩邊平分
    private val mMainPercentHorizontal: Float
    // 其他頁(yè)面相對(duì)主界面頁(yè)面最小的縮小比例
    private val mOtherViewScanMinSize: Float
    init {
        // 獲取XML參數(shù)
        val typedArray =
            context.obtainStyledAttributes(attributeSet, R.styleable.DesktopLayerLayout)
        mOrientation = typedArray.getInteger(R.styleable.DesktopLayerLayout_mOrientation,
            ORIENTATION_VERTICAL)
        isAutoFitOrientation =
            typedArray.getBoolean(R.styleable.DesktopLayerLayout_isAutoFitOrientation, true)
        mPaddingValue = typedArray.getInteger(R.styleable.DesktopLayerLayout_mPaddingValue,
            DEFAULT_PADDING_VALUE)
        mMainPercentVertical =
            typedArray.getFraction(R.styleable.DesktopLayerLayout_mMainPercentVertical,
            1, 1, DEFAULT_MAIN_PERCENT_VERTICAL)
        mMainPercentHorizontal =
            typedArray.getFraction(R.styleable.DesktopLayerLayout_mMainPercentHorizontal,
            1, 1, DEFAULT_MAIN_PERCENT_HORIZONTAL)
        mOtherViewScanMinSize =
            typedArray.getFraction(R.styleable.DesktopLayerLayout_mOtherViewScanMinSize,
            1, 1, DEFAULT_OTHER_VIEW_SCAN_SIZE)
        typedArray.recycle()
    }
    override fun onFinishInflate() {
        super.onFinishInflate()
        // 獲得所有xml內(nèi)的view,保留原始順序
        mInitViews.addAll(children)
    }
    // 屏幕方向變化并不會(huì)觸發(fā),初始時(shí)會(huì)觸發(fā),自適應(yīng)
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // Log.e("TAG", "onSizeChanged: w=$w, h=$h")
        // 根據(jù)屏幕變化修改方向,自適應(yīng)
        if (isAutoFitOrientation) {
            mOrientation = if (w > h) ORIENTATION_HORIZONTAL else ORIENTATION_VERTICAL
            requestLayout()
        }
    }
    // 需要在manifest中注冊(cè)捕捉事件類型,android:configChanges="orientation|keyboardHidden|screenSize"
    public override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        if(newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            mOrientation = ORIENTATION_VERTICAL
            requestLayout()
        }else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            mOrientation = ORIENTATION_HORIZONTAL
            requestLayout()
        }
    }
    // 排列規(guī)則:初始化第一個(gè)放中間,其他向右排列,中間最大,中心在左右邊上的最小,不可見(jiàn)的也是最小
    // view的大小應(yīng)該只和它在可見(jiàn)頁(yè)面的位置有關(guān),不應(yīng)該和curIndex有關(guān),是充分不必要關(guān)系
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 獲取默認(rèn)尺寸,考慮背景大小
        val width = max(getDefaultSize(0, widthMeasureSpec), suggestedMinimumWidth)
        val height = max(getDefaultSize(0, heightMeasureSpec), suggestedMinimumHeight)
        // 設(shè)置間距
        mGateLength = width / 4
        // 中間 view 大小
        val maxWidth: Int
        val maxHeight: Int
        // 不同方向尺寸不同
        if (mOrientation == ORIENTATION_HORIZONTAL) {
            maxWidth = (width * mMainPercentHorizontal).toInt()
            maxHeight = height - 2 * mPaddingValue
        }else {
            maxWidth = (width * mMainPercentVertical).toInt()
            maxHeight = height - 2 * mPaddingValue
        }
        // 兩側(cè) view 大小,第三排
        val minWidth = (maxWidth * mOtherViewScanMinSize).toInt()
        val minHeight = (maxHeight * mOtherViewScanMinSize).toInt()
        var childWidth: Int
        var childHeight: Int
        for (i in 0 until childCount) {
            val child = mInitViews[i]
            val scanSize = getViewScanSize(i, scrollX)
            childWidth = minWidth + ((maxWidth - minWidth) * scanSize).toInt()
            childHeight = minHeight + ((maxHeight - minHeight) * scanSize).toInt()
            // Log.e("TAG", "onMeasure($i): childWidth=$childWidth, childHeight=$childHeight")
            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY))
        }
        setMeasuredDimension(width, height)
    }
    // 選中view為最大,可見(jiàn)部分會(huì)縮放,不可見(jiàn)部分和第三排一樣大
    private fun getViewScanSize(index: Int, scrolledLen: Int): Float {
        var scanSize = 0f
        // 開(kāi)始時(shí)當(dāng)前view未測(cè)量,不計(jì)算
        if (measuredWidth == 0) return scanSize
        // 初始化的時(shí)候,第一個(gè)放中間,所以index移到可見(jiàn)范圍為[2+index, index-2],可見(jiàn)!=可移動(dòng)
        val scrollLeftLimit = (index - 2) * mGateLength
        val scrollRightLimit = (index + 2) * mGateLength
        // 先判斷child是否可見(jiàn)
        if (scrolledLen in scrollLeftLimit..scrollRightLimit) {
            // 根據(jù)二次函數(shù)計(jì)算比例
            scanSize = scanByParabola(scrollLeftLimit, scrollRightLimit, scrolledLen).toFloat()
        }
        return scanSize
    }
    // 根據(jù)拋物線計(jì)算比例,y屬于[0, 1]
    // 映射關(guān)系:(form, 0) ((from + to) / 2, 0) (to, 0) -> (0, 0) (1, 1) (2, 0)
    @Suppress("SameParameterValue")
    private fun scanByParabola(from: Int, to: Int, cur: Int): Double {
        // 公式:val y = 1 - (x - 1).toDouble().pow(2.0)
        // Log.e("TAG", "scanByParabola:from=$from, to=$to, cur=$cur ")
        val x = ((cur - from) / (to - from).toFloat() * 2).toDouble()
        return 1 - (x - 1).pow(2.0)
    }
    // layout 按順序間距排列即可,大小有onMeasure控制,開(kāi)始位置在中心,也和curIndex無(wú)關(guān)
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        val startX = (r + l) / 2
        // 排列布局
        for (i in 0 until childCount) {
            val child = mInitViews[i]
            // 中間減去間距,再減去一半的寬度,得到左邊坐標(biāo)
            val left = startX + mGateLength * i - child.measuredWidth / 2
            val top = (b + t) / 2 - child.measuredHeight / 2
            val right = left + child.measuredWidth
            val bottom = top + child.measuredHeight
            // Log.e("TAG", "onLayout($i): left=$left, right=$right")
            child.layout(left, top, right, bottom)
        }
        // 修改大小,布局完成后移動(dòng)
        scrollBy(mDxLen.toInt(), 0)
        mDxLen = 0f
        // 完成布局及移動(dòng)后,繪制之前,將可見(jiàn)view提高層級(jí)
        val targetIndex = getCurrentIndex()
        for (i in 2 downTo 0) {
            val preIndex = targetIndex - i
            val aftIndex = targetIndex + i
            // 逐次提高層級(jí),注意在mInitViews拿就可以,不可見(jiàn)不管
            if (preIndex in 0..childCount) {
                bringChildToFront(mInitViews[preIndex])
            }
            if (aftIndex != preIndex && aftIndex in 0 until childCount) {
                bringChildToFront(mInitViews[aftIndex])
            }
        }
    }
    // 根據(jù)滾動(dòng)距離獲得當(dāng)前index
    private fun getCurrentIndex()= (scrollX / mGateLength.toFloat()).roundToInt()
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        ev?.let {
            when(it.action) {
                MotionEvent.ACTION_DOWN -> {
                    mLastX = ev.x
                    if(mState == SCROLL_STATE_IDLE) {
                        mState = SCROLL_STATE_DRAGGING
                    }else if (mState == SCROLL_STATE_SETTLING) {
                        mState = SCROLL_STATE_DRAGGING
                        // 去除結(jié)束監(jiān)聽(tīng),結(jié)束動(dòng)畫(huà)
                        mValueAnimator?.removeAllListeners()
                        mValueAnimator?.cancel()
                    }
                }
                MotionEvent.ACTION_MOVE -> {
                    // 若ACTION_DOWN是本view攔截,則下面代碼不會(huì)觸發(fā),要在onTouchEvent判斷
                    val dX = mLastX - ev.x
                    return checkScrollInView(scrollX + dX)
                }
                MotionEvent.ACTION_UP -> {}
            }
        }
        return super.onInterceptHoverEvent(ev)
    }
    // 根據(jù)可以滾動(dòng)的范圍,計(jì)算是否可以滾動(dòng)
    private fun checkScrollInView(length : Float): Boolean {
        // 一層情況
        if (childCount <= 1) return false
        // 左右兩邊最大移動(dòng)值,即把最后一個(gè)移到中間
        val leftScrollLimit = 0
        val rightScrollLimit = (childCount - 1) * mGateLength
        return (length >= leftScrollLimit && length <= rightScrollLimit)
    }
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        ev?.let {
            when(it.action) {
                // 防止點(diǎn)擊空白位置或者子view未處理touch事件
                MotionEvent.ACTION_DOWN -> return true
                MotionEvent.ACTION_MOVE -> {
                    // 如果是本view攔截的ACTION_DOWN,要在此判斷
                    val dX = mLastX - ev.x
                    if(checkScrollInView(scrollX + dX)) {
                        move(ev)
                    }
                }
                MotionEvent.ACTION_UP -> moveUp()
            }
        }
        return super.onTouchEvent(ev)
    }
    private fun move(ev: MotionEvent) {
        val dX = mLastX - ev.x
        // 修改mScrollLength,重新measure及l(fā)ayout,再onLayout的最后實(shí)現(xiàn)移動(dòng)
        mDxLen += dX
        if(abs(mDxLen) >= mTouchSlop) {
            requestLayout()
        }
        // 更新值
        mLastX = ev.x
    }
    private fun moveUp() {
        // 賦值
        val targetScrollLen = getCurrentIndex() * mGateLength
        // 不能使用scroller,無(wú)法在移動(dòng)的時(shí)候進(jìn)行測(cè)量
        // mScroller.startScroll(scrollX, scrollY, (targetScrollLen - scrollX), 0)
        // 這里使用ValueAnimator處理剩余的距離,模擬滑動(dòng)到需要的位置
        val animator = ValueAnimator.ofFloat(scrollX.toFloat(), targetScrollLen.toFloat())
        animator.addUpdateListener { animation ->
            // Log.e("TAG", "stopMove: " + animation.animatedValue as Float)
            mDxLen = animation.animatedValue as Float - scrollX
            requestLayout()
        }
        // 在動(dòng)畫(huà)結(jié)束時(shí)修改curIndex
        animator.addListener (onEnd = {
            curIndex = getCurrentIndex()
            mState = SCROLL_STATE_IDLE
        })
        // 設(shè)置狀態(tài)
        mState = SCROLL_STATE_SETTLING
        animator.duration = 300L
        animator.start()
    }
}

desktop_layer_layout_style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name ="DesktopLayerLayout">
        <attr name="mOrientation">
            <enum name ="vertical" value="0" />
            <enum name ="horizontal" value="1" />
        </attr>
        <attr name="isAutoFitOrientation" format="boolean"/>
        <attr name="mPaddingValue" format="integer"/>
        <attr name="mMainPercentVertical" format="fraction"/>
        <attr name="mMainPercentHorizontal" format="fraction"/>
        <attr name="mOtherViewScanMinSize" format="fraction"/>
    </declare-styleable>
</resources>

主要問(wèn)題

這里用到的知識(shí)之前六篇文章都已經(jīng)講過(guò)了,主要就是有幾點(diǎn)實(shí)現(xiàn)起來(lái)復(fù)雜了一些,下面講講。

頁(yè)面的自動(dòng)縮放

講解頁(yè)面的縮放之前,需要先將一下頁(yè)面的擺放。這里以四分之一為間距來(lái)擺放來(lái)自XML的view,第一個(gè)view放在中間,其他都在其右邊按順序排列。

所以頁(yè)面的縮放,只和view的位置有關(guān),而view的位置又只和當(dāng)前控件左右滑動(dòng)的距離有關(guān),變量就是當(dāng)前控件橫坐標(biāo)上的滑動(dòng)值scrollX。根據(jù)view的原始index可以得到每個(gè)view可見(jiàn)時(shí)的滑動(dòng)值范圍,在通過(guò)這個(gè)范圍和實(shí)際的滑動(dòng)值scrollX,進(jìn)行映射換算得到其縮放比例。這里用到了拋物線進(jìn)行換算:

// 公式:y = 1 - (x - 1).toDouble().pow(2.0)
// 映射關(guān)系:(form, 0) ((from + to) / 2, 0) (to, 0) -> (0, 0) (1, 1) (2, 0)

滑動(dòng)范圍的限定

滑動(dòng)范圍的限定和上面類似,邊界就是第一個(gè)或者最后一個(gè)view移動(dòng)到正中間的范圍,只要實(shí)際的滑動(dòng)值scrollX在這個(gè)范圍內(nèi),那滑動(dòng)就是有效的。

頁(yè)面層級(jí)提升與恢復(fù)

頁(yè)面層級(jí)的提升在我之前文章:手撕安卓側(cè)滑欄也有用到,就是自己把view放到children的最后去,實(shí)際上ViewGroup提供了類似的功能:bringChildToFront,但是原理是一樣的。

    @Override
    public void bringChildToFront(View child) {
        final int index = indexOfChild(child);
        if (index >= 0) {
            removeFromArray(index);
            addInArray(child, mChildrenCount);
            child.mParent = this;
            requestLayout();
            invalidate();
        }
    }

這里的提升view不止一個(gè)了,而且后面還要恢復(fù),即不能打亂children的順序。所以我在onFinishInflate中用一個(gè)數(shù)組保存下這些子view的原始順序,使用的時(shí)候用這個(gè)數(shù)組就行,children里面的順序不用管,只要讓需要顯示的view放在最后就行。我這里因?yàn)殚g距是四分之一的寬度,最多可以顯示五個(gè)view,所以在onLayout的最后將這五個(gè)view得到,并按順序放到children的最后。

onDraw探討

這里我還想對(duì)onDraw探討一下,一開(kāi)始我以為既然onMeasure、onLayout中都需要去調(diào)用child的measure和layout,那能不能在onDraw里面自己去繪制child,不用自帶的,結(jié)果發(fā)現(xiàn)這是不行的。onDraw實(shí)際是View里面的一個(gè)空方法,實(shí)際對(duì)頁(yè)面的繪制是在控件的draw方法中,那重寫(xiě)draw方法自己去繪制child呢?實(shí)際也不行,當(dāng)把draw方法里面的super.draw時(shí)提示報(bào)錯(cuò):

也就是說(shuō)必須繼承super.draw這個(gè)方法,點(diǎn)開(kāi)源碼發(fā)現(xiàn),super.draw已經(jīng)把child繪制了,而且onDraw方法也是從里面?zhèn)鞒鰜?lái)的。所以沒(méi)辦法,乖乖用bringChildToFront放到children最后去,來(lái)提升層級(jí)吧,不然也不會(huì)提供這一個(gè)方法來(lái)是不是?

到此這篇關(guān)于Android實(shí)現(xiàn)自動(dòng)變換大小的ViewPager的文章就介紹到這了,更多相關(guān)Android ViewPager內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論