Android自定義view實(shí)現(xiàn)側(cè)滑欄詳解
前言
上一篇文章學(xué)了下自定義View的onDraw函數(shù)及自定義屬性,做出來(lái)的滾動(dòng)選擇控件還算不錯(cuò),就是邏輯復(fù)雜了一些。這篇文章打算利用自定義view的知識(shí),直接手撕一個(gè)安卓側(cè)滑欄,涉及到自定義LayoutParams、帶padding和margin的measure和layout、利用requestLayout實(shí)現(xiàn)動(dòng)畫(huà)效果等,有一定難度,但能重新學(xué)到很多知識(shí)!
需求
這里類似舊版QQ(我特別喜歡之前的側(cè)滑欄),有兩層頁(yè)面,滑動(dòng)不是最左側(cè)才觸發(fā)的,而是從中間頁(yè)面滑動(dòng)就觸發(fā),滑動(dòng)的時(shí)候主頁(yè)面和側(cè)滑欄頁(yè)面會(huì)以不同速度滑動(dòng),核心思路如下:
1、兩部分,主內(nèi)容和左邊側(cè)滑欄,側(cè)滑欄不完全占滿主內(nèi)容
2、在主內(nèi)容頁(yè)面向右滑動(dòng)展現(xiàn)側(cè)滑欄,同時(shí)主內(nèi)容以更慢的速度向右滑動(dòng)
3、側(cè)滑欄完全顯示時(shí)不再左滑
4、類似側(cè)滑欄,通過(guò)自定義屬性來(lái)指定側(cè)滑欄頁(yè)面,其他view為主內(nèi)容
5、側(cè)滑欄就一個(gè)view,容器內(nèi)其他view作為主內(nèi)容,view擺放類似垂直方向LinearLayout
效果圖
編寫(xiě)代碼
代碼有點(diǎn)長(zhǎng),而且有些沒(méi)用的代碼沒(méi)用注釋,不過(guò)我希望的是能通過(guò)這些沒(méi)用的代碼來(lái)說(shuō)明思路的不正確性。就像移動(dòng)時(shí)的動(dòng)畫(huà),本來(lái)我以為主內(nèi)容和側(cè)滑欄一起scrollTo就解決了,結(jié)果并不是。下面時(shí)代碼:
import android.animation.ValueAnimator import android.annotation.SuppressLint import android.content.Context import android.graphics.Canvas import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams import androidx.core.animation.addListener import androidx.core.view.forEach import com.silencefly96.module_common.R import kotlin.math.abs /** * 類似舊版QQ,兩層頁(yè)面,切換的使用有互相移動(dòng)動(dòng)畫(huà) * 核心思路 * 1、兩部分,主內(nèi)容和左邊側(cè)滑欄,側(cè)滑欄不完全占滿主內(nèi)容 * 2、在主內(nèi)容頁(yè)面向右滑動(dòng)展現(xiàn)側(cè)滑欄,同時(shí)主內(nèi)容以更慢的速度向右滑動(dòng) * 3、側(cè)滑欄完全顯示時(shí)不再左滑 * 4、類似側(cè)滑欄,通過(guò)自定義屬性來(lái)指定側(cè)滑欄頁(yè)面,其他view為主內(nèi)容 * 5、側(cè)滑欄就一個(gè)view,容器內(nèi)其他view作為主內(nèi)容,view擺放類似垂直方向LinearLayout */ @Suppress("unused") class TwoLayerSlideLayout @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = 0 ): ViewGroup(context, attributeSet, defStyleAttr){ @Suppress("unused") companion object{ //側(cè)滑共有四個(gè)方向,一個(gè)不設(shè)置的屬性,暫時(shí)只實(shí)現(xiàn)GRAVITY_TYPE_LEFT const val GRAVITY_TYPE_NULL = -1 const val GRAVITY_TYPE_LEFT = 0 const val GRAVITY_TYPE_TOP = 1 const val GRAVITY_TYPE_RIGHT = 2 const val GRAVITY_TYPE_BOTTOM = 3 //滑動(dòng)狀態(tài) const val SLIDE_STATE_TYPE_CLOSED = 0 const val SLIDE_STATE_TYPE_MOVING = 1 const val SLIDE_STATE_TYPE_OPENED = 2 } //側(cè)滑欄控件 private var mSlideView: View? = null //滑動(dòng)狀態(tài) private var mState = SLIDE_STATE_TYPE_CLOSED //最大滑動(dòng)長(zhǎng)度 private var maxScrollLength: Float //最大動(dòng)畫(huà)使用時(shí)間 private var maxAnimatorPeriod: Int //上次事件的橫坐標(biāo) private var mLastX = 0f //累計(jì)的滑動(dòng)距離 private var mScrollLength: Float = 0f //側(cè)滑欄所占比例 private var mSidePercent: Float = 0.75f //切換到目標(biāo)狀態(tài)的屬性動(dòng)畫(huà) private var mAnimator: ValueAnimator? = null init { //讀取XML參數(shù) val attrArr = context.obtainStyledAttributes(attributeSet, R.styleable.TwoLayerSlideLayout) //獲得XML里面設(shè)置的最大滑動(dòng)長(zhǎng)度,沒(méi)有的話需要在onMeasure后根據(jù)控件寬度設(shè)置 maxScrollLength = attrArr.getDimension(R.styleable.TwoLayerSlideLayout_maxScrollLength, 0f) //最大動(dòng)畫(huà)時(shí)間 maxAnimatorPeriod = attrArr.getInteger(R.styleable.TwoLayerSlideLayout_maxAnimatorPeriod, 300) //側(cè)滑欄所占比例 mSidePercent = attrArr.getFraction(R.styleable.TwoLayerSlideLayout_mSidePercent, 1,1,0.75f) attrArr.recycle() } //測(cè)量會(huì)進(jìn)行多次 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { //用默認(rèn)方法,計(jì)算出所有的childView的寬和高,帶padding不帶margin //measureChildren(widthMeasureSpec, heightMeasureSpec) //getDefaultSize會(huì)根據(jù)默認(rèn)值、模式、spec的值給到結(jié)果,建議點(diǎn)進(jìn)去看看 val width = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec) val height = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec) //類似垂直方向LinearLayout,統(tǒng)計(jì)一下垂直方向高度使用情況 //var widthUsed = 0 var heightUsed = paddingTop var childWidthMeasureSpec: Int var childHeightMeasureSpec: Int forEach { child-> //獲取設(shè)定的gravity,用于判定是否是側(cè)滑欄view,只要最后一個(gè) val childLayoutParams = child.layoutParams as LayoutParams val gravity = childLayoutParams.gravity if (gravity != GRAVITY_TYPE_NULL) { //暫不支持除左滑以外的情況 if (gravity != GRAVITY_TYPE_LEFT) throw IllegalArgumentException("function not support") //取到側(cè)滑欄,多個(gè)時(shí)取最后一個(gè) mSlideView = child //側(cè)滑欄大小另外測(cè)量,高度鋪滿父容器,寬度設(shè)置為父容器的四分之三 childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( (width * mSidePercent).toInt(), MeasureSpec.EXACTLY) //高度不限定 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) //側(cè)滑欄不帶padding和margin child.measure(childWidthMeasureSpec, childHeightMeasureSpec) }else { //寬按需求申請(qǐng),所以應(yīng)該用AT_MOST,并向下層view傳遞 childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST) childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST) //heightUsed會(huì)在getChildMeasureSpec中用到,MATCH_PARENT時(shí)占滿剩余size //WRAP_CONTENT時(shí),會(huì)帶著MeasureSpec.AT_MOST及剩余size向下層傳遞 //帶padding和margin的測(cè)量,推薦看看measureChildWithMargins //里面用到的getChildMeasureSpec函數(shù),加深對(duì)MeasureSpec理解 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed) //計(jì)算的時(shí)候要加上child的margin值 //widthUsed += child.measuredWidth heightUsed += child.measuredHeight + childLayoutParams.topMargin + childLayoutParams.bottomMargin } } //最后加上本控件的paddingBottom,最終計(jì)算得到最終高度 heightUsed += paddingBottom //設(shè)置最大滑動(dòng)長(zhǎng)度為寬度的三分之一 if (maxScrollLength == 0f) { maxScrollLength = width / 3f } //設(shè)置測(cè)量參數(shù),這里不能用heightUsed,因?yàn)殡m然主內(nèi)容可能未用完height,但是側(cè)滑欄用完了height setMeasuredDimension(width, height) } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { //滑動(dòng)時(shí)的偏移值,計(jì)算dx的時(shí)候時(shí)前面減后面,這里偏移值應(yīng)該是后面減前面,所以取負(fù) val mainOffset = -mScrollLength / maxScrollLength * measuredWidth * (1 - mSidePercent) val slideOffset = -mScrollLength / maxScrollLength * (measuredWidth * mSidePercent) //不要忘記了paddingTop和paddingLeft,不然內(nèi)容會(huì)被padding的背景覆蓋 var curHeight = paddingTop //布局 var layoutParams: LayoutParams var gravity: Int var cTop: Int var cRight: Int var cLeft: Int var cBottom: Int forEach { child -> //獲取設(shè)定的gravity,用于判定是否是側(cè)滑欄view,只要最后一個(gè) layoutParams = child.layoutParams as LayoutParams gravity = layoutParams.gravity //布局主內(nèi)容中view if (gravity == GRAVITY_TYPE_NULL) { //其他view帶上累加高度布局 cTop = layoutParams.topMargin + curHeight cLeft = paddingLeft + layoutParams.leftMargin + mainOffset.toInt() cRight = cLeft + child.measuredWidth cBottom = cTop + child.measuredHeight //布局 child.layout(cLeft, cTop, cRight, cBottom) //累加高度 curHeight = cBottom + layoutParams.bottomMargin } } //最后繪制側(cè)滑欄,使其在最頂層???這里直接layout是沒(méi)用的,繪想想看,繪制是onDraw的職責(zé),這里有兩個(gè)種辦法 //一是在XML中將側(cè)滑欄放到最后去,二是將mSlideView放到children的最后去,onDraw內(nèi)應(yīng)該是for循環(huán)繪制的 mSlideView?.let { //下面方法是專門在onLayout方法中使用的,不會(huì)觸發(fā)requestLayout removeViewInLayout(mSlideView) addViewInLayout(mSlideView!!, childCount, mSlideView!!.layoutParams) //這里還有一個(gè)問(wèn)題,當(dāng)當(dāng)前view設(shè)置padding的時(shí)候,側(cè)滑欄會(huì)被裁切,設(shè)置不裁切padding內(nèi)容 this.layoutParams.apply { //不裁切孫view在父view超出的部分,讓孫view在爺爺view中正常顯示,這里不需要 //clipChildren = false clipToPadding = false } //在頁(yè)面左邊 cTop = 0 cRight = slideOffset.toInt() cLeft = cRight - mSlideView!!.measuredWidth cBottom = cTop + mSlideView!!.measuredHeight //布局 mSlideView!!.layout(cLeft, cTop, cRight, cBottom) } } override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { ev?.let { when(ev.action) { MotionEvent.ACTION_DOWN -> preMove(ev) MotionEvent.ACTION_MOVE -> return true } } return super.onInterceptTouchEvent(ev) } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(ev: MotionEvent?): Boolean { ev?.let { when(ev.action) { //如果子控件未攔截ACTION_DOWN事件或者點(diǎn)擊在view沒(méi)有子控件的地方,onTouchEvent要處理 MotionEvent.ACTION_DOWN -> { //preMove(ev) return true } MotionEvent.ACTION_MOVE -> moveView(ev) MotionEvent.ACTION_UP -> stopMove() } } return super.onTouchEvent(ev) } private fun preMove(e: MotionEvent) { mLastX = e.x if (mState == SLIDE_STATE_TYPE_MOVING) { //要取消結(jié)束監(jiān)聽(tīng),防止錯(cuò)誤修改狀態(tài),把當(dāng)前位置交給接下來(lái)的滑動(dòng)處理 mAnimator?.removeAllListeners() mAnimator?.cancel() }else { //關(guān)閉和展開(kāi)時(shí),點(diǎn)擊滑動(dòng)應(yīng)該切換狀態(tài) mState = SLIDE_STATE_TYPE_MOVING } } private fun moveView(e: MotionEvent) { //沒(méi)有側(cè)滑欄不移動(dòng),避免多次請(qǐng)求布局 if (mSlideView == null) return //注意前面減去后面,就是頁(yè)面應(yīng)該scroll的值 val dx = mLastX - e.x mLastX = e.x //Log.e("TAG", "moveView: mScrollLength=$mScrollLength") //設(shè)定滑動(dòng)范圍,注意mScrollLength和scrollX是不一樣的,我們要實(shí)現(xiàn)不同的滑動(dòng)效果 //注意滑動(dòng)的是窗口,view是窗口下的內(nèi)容,手指向右滑動(dòng),頁(yè)面(即主內(nèi)容)向左移動(dòng),窗口向右移動(dòng) if ((mScrollLength + dx) >= -maxScrollLength && (mScrollLength + dx) <= 0) { //范圍內(nèi),疊加差值 mScrollLength += dx //手指向右滑動(dòng),主內(nèi)容向左緩慢滑動(dòng),側(cè)滑欄向右滑動(dòng) //要體現(xiàn)更慢的速度,主內(nèi)容就移動(dòng)側(cè)滑欄所占比例的剩余值 //val mainDx = dx / maxScrollLength * measuredWidth * (1 - mSidePercent) //scrollBy(mainDx.toInt(), 0) //側(cè)滑欄速度更大,這里據(jù)最大滑動(dòng)距離和側(cè)滑欄的寬度做個(gè)映射 //val sideDx = dx / maxScrollLength * (measuredWidth * mSidePercent) //側(cè)滑欄的移動(dòng)不能使用scrollTo和scrollBy,因?yàn)閮H僅移動(dòng)的是其中的內(nèi)容,并不會(huì)移動(dòng)整個(gè)view //可以理解成scrollTo和scrollBy只是在該對(duì)象的原有位置移動(dòng),即使移動(dòng)了也不會(huì)在其范圍之外顯示(draw) //屬性動(dòng)畫(huà)可以實(shí)現(xiàn)在父容器里面對(duì)子控件的移動(dòng),但是也是通過(guò)修改屬性值重新布局實(shí)現(xiàn)的 //sideView!!.scrollTo(sideView!!.scrollX + sideDx.toInt(), 0) //這里累加mScrollLength后直接請(qǐng)求重新布局,在onLayout里面去處理移動(dòng) requestLayout() } } private fun stopMove() { //停止后,使用動(dòng)畫(huà)移動(dòng)到目標(biāo)位置 val terminalScrollX: Float = if (abs(mScrollLength) >= maxScrollLength / 2f) { //觸發(fā)移動(dòng)至完全展開(kāi),mScrollLength是個(gè)負(fù)數(shù) -maxScrollLength }else { //如果移動(dòng)沒(méi)過(guò)半應(yīng)該恢復(fù)狀態(tài),則恢復(fù)到原來(lái)狀態(tài) 0f } //這里使用ValueAnimator處理剩余的距離,模擬滑動(dòng)到需要的位置 mAnimator = ValueAnimator.ofFloat(mScrollLength, terminalScrollX) mAnimator!!.addUpdateListener { animation -> mScrollLength = animation.animatedValue as Float //請(qǐng)求重新布局 requestLayout() } //動(dòng)畫(huà)結(jié)束時(shí)要更新?tīng)顟B(tài) mAnimator!!.addListener (onEnd = { mState = if(mScrollLength == 0f) SLIDE_STATE_TYPE_CLOSED else SLIDE_STATE_TYPE_OPENED }) //滑動(dòng)動(dòng)畫(huà)總時(shí)間應(yīng)該和距離有關(guān) val percent = 1 - abs(mScrollLength / maxScrollLength) mAnimator!!.duration = (maxAnimatorPeriod * abs(percent)).toLong() //mAnimator.duration = maxAnimatorPeriod.toLong() mAnimator!!.start() } //自定義的LayoutParams,子控件使用的是父控件的LayoutParams,所以父控件可以增加自己的屬性,在子控件XML中使用 @Suppress("MemberVisibilityCanBePrivate") class LayoutParams : MarginLayoutParams { //側(cè)滑欄方向,不設(shè)置就是null var gravity: Int = GRAVITY_TYPE_NULL //三個(gè)構(gòu)造 constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { //讀取XML參數(shù),設(shè)置相關(guān)屬性,這里有個(gè)很煩的warning,樣式必須是外部類加layout結(jié)尾 val attrArr = context.obtainStyledAttributes(attrs, R.styleable.TwoLayerSlideLayout_Layout) gravity = attrArr.getInteger( R.styleable.TwoLayerSlideLayout_Layout_slide_gravity, GRAVITY_TYPE_NULL) //回收 attrArr.recycle() } constructor(width: Int, height: Int) : super(width, height) constructor(source: ViewGroup.LayoutParams) : super(source) } //重寫(xiě)下面四個(gè)函數(shù),在布局文件被填充為對(duì)象的時(shí)候調(diào)用的 override fun generateLayoutParams(attrs: AttributeSet): ViewGroup.LayoutParams { return LayoutParams(context, attrs) } override fun generateLayoutParams(p: ViewGroup.LayoutParams?): ViewGroup.LayoutParams { return LayoutParams(p) } override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams { return LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) } override fun checkLayoutParams(p: ViewGroup.LayoutParams?): Boolean { return p is LayoutParams } }
下面是配合使用的XML屬性代碼:
res->value->two_layer_slide_layout_style.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name ="TwoLayerSlideLayout"> <attr name="maxScrollLength" format="dimension"/> <attr name="maxAnimatorPeriod" format="integer"/> <attr name="mSidePercent" format="fraction"/> </declare-styleable> <declare-styleable name ="TwoLayerSlideLayout.Layout"> <attr name ="slide_gravity"> <enum name ="left" value="0" /> <enum name ="top" value="1" /> <enum name ="right" value="2" /> <enum name ="bottom" value="3" /> </attr > </declare-styleable> </resources>
使用時(shí)在XML里面的例子,kotlin代碼幾乎不用寫(xiě)了,注意命名空間是app,res-auto引入了我們的屬性:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.silencefly96.module_common.view.TwoLayerSlideLayout android:id="@+id/hhView" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/teal_700" android:padding="50dp" app:mSidePercent="75%" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> <LinearLayout app:slide_gravity="left" android:background="@color/teal_200" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:text="@string/test_string" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout> <TextView android:background="@color/purple_200" android:layout_marginTop="10dp" android:text="@string/app_name" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:background="@color/purple_200" android:layout_marginTop="10dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:text="@string/app_name" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:background="@color/purple_200" android:layout_marginTop="50dp" android:text="@string/test_string" android:layout_width="match_parent" android:layout_height="wrap_content"/> </com.silencefly96.module_common.view.TwoLayerSlideLayout> </androidx.constraintlayout.widget.ConstraintLayout>
主要問(wèn)題
說(shuō)起這個(gè)控件,問(wèn)題可就很多了,當(dāng)然學(xué)到的東西也特別多,下面好好講講。
自定義XML中Fraction的使用
上一篇文章實(shí)際也用到了這個(gè)類型的屬性,即百分比,可是我沒(méi)測(cè)試下。在這個(gè)控件里面自己設(shè)置了一下,發(fā)現(xiàn)這個(gè)并不是像我想象填小數(shù)或者100內(nèi)的整數(shù),而是填完百分比后還要自己加一個(gè)百分號(hào)“%”!至于getFraction里面的base和pbase可以自己搜一下,我這就不展開(kāi)講了,畢竟主要內(nèi)容是自定義view。
View提供的getDefaultSize
前面都是自己寫(xiě)一個(gè)getSizeFromMeasureSpec函數(shù)來(lái)根據(jù)MeasureSpec模式獲得size,沒(méi)想到View中已經(jīng)提供了一個(gè)一模一樣的功能,尷尬了。
自定義LayoutParams
這個(gè)是這篇文章的重頭戲了,沒(méi)學(xué)習(xí)之前,我是萬(wàn)萬(wàn)沒(méi)想到一個(gè)View的LayoutParams屬性居然是父viewgroup的LayoutParams類型,而且自定義Viewgroup的同時(shí)還得自定義自身的LayoutParams,不然LayoutParams就一個(gè)height和一個(gè)width參數(shù)。話不多說(shuō),下面大致講講,詳細(xì)的還是找資料再補(bǔ)充下!
理解
關(guān)于一個(gè)View的LayoutParams屬性居然是父viewgroup的LayoutParams類型的描述,其實(shí)也很好理解,想想經(jīng)常用到的ConstraintLayout,我能不就是在它的子view中設(shè)置約束屬性么。所以我們要實(shí)現(xiàn)一個(gè)Layout,那子view不就是使用Layout的LayoutParams么。更何況哪面試經(jīng)常問(wèn)的問(wèn)題來(lái)說(shuō),一個(gè)view的寬高受什么影響,不就是父viewgroup的MeasureSpec和子view的LayoutParams決定的么,子view要對(duì)父view進(jìn)行約束,那不就得知道父view需要控制什么屬性么!
好了上面是我的理解,下面開(kāi)始說(shuō)明怎么使用。
LayoutParams需求
首先我們這里要實(shí)現(xiàn)一個(gè)類似官方側(cè)滑欄的功能,相信大家都用過(guò)DrawerLayout,在DrawerLayout里面我們通過(guò)指定一個(gè)子view的layout_gravity就能讓它成為側(cè)滑欄,沒(méi)錯(cuò),我們這也想實(shí)現(xiàn)這樣的效果。一開(kāi)始我就直接寫(xiě)嘛,app:layout_gravity不就是官方的么,可是我在XML中輸入這樣一個(gè)屬性,在onMeasure里面讀取不就可以判定了。結(jié)果代碼中的LayoutParams只有height和width兩個(gè)參數(shù),這麻煩了,找了下資料,原來(lái)要自己定義Viewgroup的LayoutParams!
自定義LayoutParams
這里就大致講下思路,代碼里面注釋寫(xiě)的很清楚,分三步吧。第一步是要在代碼中創(chuàng)建一個(gè)自定義的LayoutParams,這里我就直接寫(xiě)成內(nèi)部類了,實(shí)現(xiàn)其中幾個(gè)構(gòu)造函數(shù),并在構(gòu)造里面讀取到要用的參數(shù);第二步就是自定義參數(shù)了,需要?jiǎng)?chuàng)建一個(gè)xml文件來(lái)定義參數(shù),這里用到了枚舉類型的屬性,并且代碼里面也要定義好各種type,LayoutParams類中定義一個(gè)變量來(lái)儲(chǔ)存這個(gè)屬性;第三步就是重寫(xiě)在布局文件被填充為對(duì)象的時(shí)候調(diào)用的幾個(gè)函數(shù),就大功告成了。
使用的時(shí)候要自己強(qiáng)制轉(zhuǎn)換一下,就能從子view的LayoutParams中拿到自定義的屬性了。
帶padding和margin的測(cè)量
側(cè)滑欄應(yīng)該占滿屏幕,不應(yīng)該帶padding和margin,另外測(cè)量就行,很簡(jiǎn)單。主內(nèi)容部分我們要實(shí)現(xiàn)類似LinearLayout的效果,就得帶上帶padding和margin進(jìn)行測(cè)量。
這里用到了measureChildWithMargins這個(gè)函數(shù),他會(huì)接收child、MeasureSpec及寬高的使用情況對(duì)child進(jìn)行帶padding和margin的測(cè)量,可以點(diǎn)進(jìn)去看看這個(gè)函數(shù),里面又會(huì)調(diào)用getChildMeasureSpec去獲得child的MeasureSpec,根據(jù)MeasureSpec的三種類型及LayoutParams.layout_width/height的三種形式(確切值、wrap_content、match_parent),會(huì)產(chǎn)生九種不同的組合。
不過(guò)可以理解的是,控件如果設(shè)置了值那就是設(shè)置的值(三種情況);如果控件是match_parent,那EXACTLY和AT_MOST的值都會(huì)被該view用完(兩種情況),如果是UNSPECIFIED就要特殊處理了(一種情況);如果控件是wrap_content,在EXACTLY和AT_MOST里面,都會(huì)用給的值和AT_MOST生成一個(gè)新的MeasureSpec,并向下層傳遞下去,即wrap_content不知道要多大,但是知道最大有多大,下層的view按需索求(兩種情況),在UNSPECIFIED里也是特殊處理下(一種情況)。
這里還有個(gè)heightUsed要注意下,累加的高度應(yīng)該是父容器的padding,加上子控件的margin及高度共同構(gòu)成的。我這里只統(tǒng)計(jì)了高度,寬度上也是同理。在這里的setMeasuredDimension函數(shù)中,用的是整個(gè)控件最大的高度,而不是heightUsed,因?yàn)閭?cè)滑欄占滿了控件的高度。但是如果我們僅僅是實(shí)現(xiàn)一個(gè)LinarLayout的話,就應(yīng)該用這個(gè)heightUsed了。
帶padding和margin的布局
這里和上面測(cè)量類似,要帶上父容器的padding和子控件的margin以及子控件的寬高進(jìn)行擺放。這里暫時(shí)不涉及動(dòng)畫(huà)的話,就是要把各個(gè)child的left、top、right、bottom四個(gè)值計(jì)算清楚,同時(shí)注意curHeight的累加就行了。
側(cè)滑欄被主內(nèi)容里面控件覆蓋顯示問(wèn)題
這里有個(gè)很奇怪的問(wèn)題,就是側(cè)滑欄會(huì)被主內(nèi)容里面控件覆蓋顯示,側(cè)滑欄可以覆蓋主內(nèi)容的背景,但是主內(nèi)容里面的控件會(huì)在側(cè)滑欄上面繪制。這里我把側(cè)滑欄的view從XML第一個(gè)移到最后一個(gè)就沒(méi)事了,可是這不符合我們的邏輯,我又在onLayout里面最后去layout側(cè)滑欄,結(jié)果還是不行。后面想想繪制應(yīng)該是在draw里面吧,可能是直接for循環(huán)繪制的,我用iterator移除再添加到最后去不就行了,后面發(fā)現(xiàn)children的iterator并未提供刪除的功能,最后還是發(fā)現(xiàn)了removeViewInLayout和addViewInLayout兩個(gè)函數(shù),是專門在onLayout里面使用的,按前面的邏輯試一下,果然就好了。
設(shè)置padding被裁切的問(wèn)題
這里如果在我們的TwoLayerSlideLayout上設(shè)置padding,那就會(huì)出現(xiàn)很神奇的效果,側(cè)滑欄也有padding了,但是仔細(xì)看,側(cè)滑欄的內(nèi)容位置是沒(méi)錯(cuò)的,就是有padding的位置,側(cè)滑欄的內(nèi)容會(huì)被主內(nèi)容的背景覆蓋。查了下資料,又學(xué)了幾個(gè)東西,主要就是viewgroup的layoutParams里面有個(gè)clipToPadding屬性,默認(rèn)為true,會(huì)將padding部分的子view進(jìn)行裁切,我們?cè)趥?cè)滑欄layout前把它設(shè)置為false就行了。
滑動(dòng)不生效問(wèn)題
如果看了我前面的文章,在帶header和footer的滾動(dòng)控件中,中間滾動(dòng)的控件是TextView,也是無(wú)法移動(dòng),在那里我是通過(guò)設(shè)置clickable為true讓TextView也會(huì)消耗ACTION_DOWN事件,從而保證viewgroup能收到move事件。在寫(xiě)當(dāng)前控件的時(shí)候,不僅是里面的TextView不會(huì)消耗ACTION_DOWN事件了,而且因?yàn)槲覀僾iew還有很多是沒(méi)有子view的空隙,點(diǎn)擊在這些空隙里面同樣不會(huì)消耗ACTION_DOWN事件,導(dǎo)致事件序列被丟棄,ACTION_MOVE事件也沒(méi)了。
后面想想,好像還挺好解決的,之前沒(méi)思考光去考慮TextView了,如果子控件沒(méi)消耗耗ACTION_DOWN事件,事件會(huì)交到它的父控件的onTouchEvent處理,面試過(guò)的都知道,辦法補(bǔ)救在這里嗎?無(wú)論是子控件未消耗,還是點(diǎn)擊在空隙上,最終都會(huì)把ACTION_DOWN事件交到當(dāng)前控件的onTouchEvent方法內(nèi),我們?cè)谶@里return true就可以了。
側(cè)滑欄的移動(dòng)
前面幾篇文章都做過(guò)移動(dòng)的處理了,這個(gè)view我開(kāi)始也是照搬代碼,使用scrollBy去移動(dòng),側(cè)滑欄在主內(nèi)容移動(dòng)的基礎(chǔ)上繼續(xù)通過(guò)scrollBy移動(dòng),結(jié)果想法很好,還計(jì)算了一系列值,最后發(fā)現(xiàn)只有主內(nèi)容會(huì)移動(dòng)。實(shí)際想了想,我調(diào)用側(cè)滑欄的scrollBy去移動(dòng),移動(dòng)的也只是側(cè)滑欄的內(nèi)容啊,也就是說(shuō)移動(dòng)是在側(cè)滑欄內(nèi)部進(jìn)行的,又繼續(xù)看了下滑動(dòng)效果,果然側(cè)滑欄雖然沒(méi)有被scrollBy滑動(dòng)覆蓋主內(nèi)容,但是側(cè)滑欄里面的內(nèi)容確實(shí)是以我設(shè)計(jì)的速度進(jìn)行的。
寫(xiě)道這里我又想到了上面的clipToPadding屬性,viewgroup的layoutParams還有一個(gè)clipChildren屬性,就是不裁切不裁切孫view在父view超出的部分,可是就算側(cè)滑欄里面的控件移動(dòng)到了主內(nèi)容上面,效果也還是不對(duì)的,因?yàn)閭?cè)滑欄的背景并沒(méi)有移動(dòng),也就是說(shuō)這是不可行的。
這里我想到了屬性動(dòng)畫(huà),屬性動(dòng)畫(huà)是可以讓整個(gè)view移動(dòng)的,但是在每一個(gè)move事件里面去創(chuàng)建一個(gè)屬性動(dòng)畫(huà),每次移動(dòng)一小部分嗎?好像不太好,而且既然屬性動(dòng)畫(huà)是根據(jù)屬性去修改位置的,我們直接去修改布局不就行了。這里據(jù)滑動(dòng)值,計(jì)算出主內(nèi)容和側(cè)滑欄的偏移,然后使用requestLayout重新布局就可以了,布局的時(shí)候加上偏移,代碼很簡(jiǎn)單。
滑動(dòng)停止切換到目標(biāo)位置
這里和前面幾個(gè)view一樣,用ValueAnimator來(lái)模擬繼續(xù)滑動(dòng),但是上一篇文章中滾動(dòng)選擇控件會(huì)因?yàn)閯?dòng)畫(huà)沒(méi)結(jié)束有繼續(xù)滑動(dòng)導(dǎo)致出現(xiàn)滑出界的問(wèn)題,這里解決下。主要就是增加了一個(gè)狀態(tài)的判定,分三個(gè)狀態(tài),如果動(dòng)畫(huà)沒(méi)有結(jié)束,就點(diǎn)擊進(jìn)行滑動(dòng),在ACTION_DOWN事件時(shí)就把動(dòng)畫(huà)停了,并移除結(jié)束監(jiān)聽(tīng)回調(diào),這時(shí)候并不會(huì)修改mScrollLength,可以繼續(xù)交給新的滑動(dòng)接管整個(gè)滑動(dòng)過(guò)程,這樣用起來(lái)就流暢多了!
滑動(dòng)速度問(wèn)題
val mainOffset = -mScrollLength / maxScrollLength * measuredWidth * (1 - mSidePercent) val slideOffset = -mScrollLength / maxScrollLength * (measuredWidth * mSidePercent)
上面是我們主內(nèi)容和側(cè)滑欄偏移的計(jì)算代碼,邏輯是我們?cè)O(shè)定一個(gè)讓側(cè)滑欄展開(kāi)的最大滑動(dòng)距離,滑動(dòng)的時(shí)候側(cè)滑欄按滑動(dòng)距離占最大滑動(dòng)距離的比例去展開(kāi)側(cè)滑欄,也就是說(shuō)滑動(dòng)距離等于最大滑動(dòng)距離時(shí)就展開(kāi)了,中間按比例移動(dòng);對(duì)于主內(nèi)容,我們就讓它移動(dòng)的最大距離為側(cè)滑欄所占屏幕寬度的剩余值,也就是說(shuō)滑動(dòng)距離等于最大滑動(dòng)距離時(shí)主內(nèi)容就移動(dòng)了側(cè)滑欄占屏幕寬度的剩余值,中間同樣時(shí)按比例移動(dòng)。稍微理解下,很簡(jiǎn)單,如果側(cè)滑欄占屏幕寬度的比例大于一半,那側(cè)滑欄速度就比主內(nèi)容大,反之主內(nèi)容速度大,實(shí)際上這樣也很合理!
到此這篇關(guān)于Android自定義view實(shí)現(xiàn)側(cè)滑欄詳解的文章就介紹到這了,更多相關(guān)Android側(cè)滑欄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android仿微信語(yǔ)音聊天界面設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了Android仿微信語(yǔ)音聊天界面設(shè)計(jì)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android創(chuàng)建簡(jiǎn)單發(fā)送和接收短信應(yīng)用
收發(fā)短信應(yīng)該是每個(gè)手機(jī)最基本的功能之一了,即使是許多年前的老手機(jī)也都會(huì)具備這項(xiàng)功能,而Android 作為出色的智能手機(jī)操作系統(tǒng),自然也少不了在這方面的支持。今天我們開(kāi)始自己創(chuàng)建一個(gè)簡(jiǎn)單的發(fā)送和接收短信的應(yīng)用,需要的朋友可以參考下2016-04-04Android 多媒體播放API簡(jiǎn)單實(shí)例
這篇文章主要介紹了Android 多媒體播放API簡(jiǎn)單實(shí)例的相關(guān)資料,這里附有代碼實(shí)例及實(shí)現(xiàn)效果圖,需要的朋友可以參考下2016-12-12Android中用StaticLayout實(shí)現(xiàn)文本繪制自動(dòng)換行詳解
StaticLayout是android中處理文字換行的一個(gè)工具類,StaticLayout已經(jīng)實(shí)現(xiàn)了文本繪制換行處理,下面這篇文章主要介紹了Android中用StaticLayout實(shí)現(xiàn)文本繪制自動(dòng)換行的相關(guān)資料,需要的朋友可以參考。2017-03-03Android視頻加水印之FFmpeg的簡(jiǎn)單應(yīng)用實(shí)例
最近有個(gè)需求,需要錄制視頻,能添加水印,所以下面這篇文章主要給大家介紹了關(guān)于Android視頻加水印之FFmpeg的簡(jiǎn)單應(yīng)用的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05如何玩轉(zhuǎn)Android矢量圖VectorDrawable
這篇文章主要教大家如何玩轉(zhuǎn)Android矢量圖VectorDrawable,對(duì)矢量圖感興趣的小伙伴們可以參考一下2016-05-05Android實(shí)現(xiàn)拍照及圖片裁剪(6.0以上權(quán)限處理及7.0以上文件管理)
本篇文章主要介紹了Android實(shí)現(xiàn)拍照及圖片裁剪(6.0以上權(quán)限處理及7.0以上文件管理),非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-10-10android全局監(jiān)控click事件的四種方式(小結(jié))
本篇文章主要介紹了android全局監(jiān)控click事件的四種方式(小結(jié)),詳細(xì)介紹如何在全局上去監(jiān)聽(tīng) click 點(diǎn)擊事件,并做些通用處理或是攔截,有興趣的可以了解一下2017-08-08