Android自定義view實現(xiàn)側(cè)滑欄詳解
前言
上一篇文章學(xué)了下自定義View的onDraw函數(shù)及自定義屬性,做出來的滾動選擇控件還算不錯,就是邏輯復(fù)雜了一些。這篇文章打算利用自定義view的知識,直接手撕一個安卓側(cè)滑欄,涉及到自定義LayoutParams、帶padding和margin的measure和layout、利用requestLayout實現(xiàn)動畫效果等,有一定難度,但能重新學(xué)到很多知識!
需求
這里類似舊版QQ(我特別喜歡之前的側(cè)滑欄),有兩層頁面,滑動不是最左側(cè)才觸發(fā)的,而是從中間頁面滑動就觸發(fā),滑動的時候主頁面和側(cè)滑欄頁面會以不同速度滑動,核心思路如下:
1、兩部分,主內(nèi)容和左邊側(cè)滑欄,側(cè)滑欄不完全占滿主內(nèi)容
2、在主內(nèi)容頁面向右滑動展現(xiàn)側(cè)滑欄,同時主內(nèi)容以更慢的速度向右滑動
3、側(cè)滑欄完全顯示時不再左滑
4、類似側(cè)滑欄,通過自定義屬性來指定側(cè)滑欄頁面,其他view為主內(nèi)容
5、側(cè)滑欄就一個view,容器內(nèi)其他view作為主內(nèi)容,view擺放類似垂直方向LinearLayout
效果圖

編寫代碼
代碼有點長,而且有些沒用的代碼沒用注釋,不過我希望的是能通過這些沒用的代碼來說明思路的不正確性。就像移動時的動畫,本來我以為主內(nèi)容和側(cè)滑欄一起scrollTo就解決了,結(jié)果并不是。下面時代碼:
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,兩層頁面,切換的使用有互相移動動畫
* 核心思路
* 1、兩部分,主內(nèi)容和左邊側(cè)滑欄,側(cè)滑欄不完全占滿主內(nèi)容
* 2、在主內(nèi)容頁面向右滑動展現(xiàn)側(cè)滑欄,同時主內(nèi)容以更慢的速度向右滑動
* 3、側(cè)滑欄完全顯示時不再左滑
* 4、類似側(cè)滑欄,通過自定義屬性來指定側(cè)滑欄頁面,其他view為主內(nèi)容
* 5、側(cè)滑欄就一個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è)滑共有四個方向,一個不設(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
//滑動狀態(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
//滑動狀態(tài)
private var mState = SLIDE_STATE_TYPE_CLOSED
//最大滑動長度
private var maxScrollLength: Float
//最大動畫使用時間
private var maxAnimatorPeriod: Int
//上次事件的橫坐標(biāo)
private var mLastX = 0f
//累計的滑動距離
private var mScrollLength: Float = 0f
//側(cè)滑欄所占比例
private var mSidePercent: Float = 0.75f
//切換到目標(biāo)狀態(tài)的屬性動畫
private var mAnimator: ValueAnimator? = null
init {
//讀取XML參數(shù)
val attrArr = context.obtainStyledAttributes(attributeSet, R.styleable.TwoLayerSlideLayout)
//獲得XML里面設(shè)置的最大滑動長度,沒有的話需要在onMeasure后根據(jù)控件寬度設(shè)置
maxScrollLength = attrArr.getDimension(R.styleable.TwoLayerSlideLayout_maxScrollLength,
0f)
//最大動畫時間
maxAnimatorPeriod = attrArr.getInteger(R.styleable.TwoLayerSlideLayout_maxAnimatorPeriod,
300)
//側(cè)滑欄所占比例
mSidePercent = attrArr.getFraction(R.styleable.TwoLayerSlideLayout_mSidePercent,
1,1,0.75f)
attrArr.recycle()
}
//測量會進(jìn)行多次
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//用默認(rèn)方法,計算出所有的childView的寬和高,帶padding不帶margin
//measureChildren(widthMeasureSpec, heightMeasureSpec)
//getDefaultSize會根據(jù)默認(rèn)值、模式、spec的值給到結(jié)果,建議點進(jìn)去看看
val width = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)
val height = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec)
//類似垂直方向LinearLayout,統(tǒng)計一下垂直方向高度使用情況
//var widthUsed = 0
var heightUsed = paddingTop
var childWidthMeasureSpec: Int
var childHeightMeasureSpec: Int
forEach { child->
//獲取設(shè)定的gravity,用于判定是否是側(cè)滑欄view,只要最后一個
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è)滑欄,多個時取最后一個
mSlideView = child
//側(cè)滑欄大小另外測量,高度鋪滿父容器,寬度設(shè)置為父容器的四分之三
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
(width * mSidePercent).toInt(), MeasureSpec.EXACTLY)
//高度不限定
childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
//側(cè)滑欄不帶padding和margin
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}else {
//寬按需求申請,所以應(yīng)該用AT_MOST,并向下層view傳遞
childWidthMeasureSpec =
MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST)
childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)
//heightUsed會在getChildMeasureSpec中用到,MATCH_PARENT時占滿剩余size
//WRAP_CONTENT時,會帶著MeasureSpec.AT_MOST及剩余size向下層傳遞
//帶padding和margin的測量,推薦看看measureChildWithMargins
//里面用到的getChildMeasureSpec函數(shù),加深對MeasureSpec理解
measureChildWithMargins(child, widthMeasureSpec, 0,
heightMeasureSpec, heightUsed)
//計算的時候要加上child的margin值
//widthUsed += child.measuredWidth
heightUsed += child.measuredHeight +
childLayoutParams.topMargin + childLayoutParams.bottomMargin
}
}
//最后加上本控件的paddingBottom,最終計算得到最終高度
heightUsed += paddingBottom
//設(shè)置最大滑動長度為寬度的三分之一
if (maxScrollLength == 0f) {
maxScrollLength = width / 3f
}
//設(shè)置測量參數(shù),這里不能用heightUsed,因為雖然主內(nèi)容可能未用完height,但是側(cè)滑欄用完了height
setMeasuredDimension(width, height)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
//滑動時的偏移值,計算dx的時候時前面減后面,這里偏移值應(yīng)該是后面減前面,所以取負(fù)
val mainOffset = -mScrollLength / maxScrollLength * measuredWidth * (1 - mSidePercent)
val slideOffset = -mScrollLength / maxScrollLength * (measuredWidth * mSidePercent)
//不要忘記了paddingTop和paddingLeft,不然內(nèi)容會被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,只要最后一個
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是沒用的,繪想想看,繪制是onDraw的職責(zé),這里有兩個種辦法
//一是在XML中將側(cè)滑欄放到最后去,二是將mSlideView放到children的最后去,onDraw內(nèi)應(yīng)該是for循環(huán)繪制的
mSlideView?.let {
//下面方法是專門在onLayout方法中使用的,不會觸發(fā)requestLayout
removeViewInLayout(mSlideView)
addViewInLayout(mSlideView!!, childCount, mSlideView!!.layoutParams)
//這里還有一個問題,當(dāng)當(dāng)前view設(shè)置padding的時候,側(cè)滑欄會被裁切,設(shè)置不裁切padding內(nèi)容
this.layoutParams.apply {
//不裁切孫view在父view超出的部分,讓孫view在爺爺view中正常顯示,這里不需要
//clipChildren = false
clipToPadding = false
}
//在頁面左邊
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事件或者點擊在view沒有子控件的地方,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ài),把當(dāng)前位置交給接下來的滑動處理
mAnimator?.removeAllListeners()
mAnimator?.cancel()
}else {
//關(guān)閉和展開時,點擊滑動應(yīng)該切換狀態(tài)
mState = SLIDE_STATE_TYPE_MOVING
}
}
private fun moveView(e: MotionEvent) {
//沒有側(cè)滑欄不移動,避免多次請求布局
if (mSlideView == null) return
//注意前面減去后面,就是頁面應(yīng)該scroll的值
val dx = mLastX - e.x
mLastX = e.x
//Log.e("TAG", "moveView: mScrollLength=$mScrollLength")
//設(shè)定滑動范圍,注意mScrollLength和scrollX是不一樣的,我們要實現(xiàn)不同的滑動效果
//注意滑動的是窗口,view是窗口下的內(nèi)容,手指向右滑動,頁面(即主內(nèi)容)向左移動,窗口向右移動
if ((mScrollLength + dx) >= -maxScrollLength && (mScrollLength + dx) <= 0) {
//范圍內(nèi),疊加差值
mScrollLength += dx
//手指向右滑動,主內(nèi)容向左緩慢滑動,側(cè)滑欄向右滑動
//要體現(xiàn)更慢的速度,主內(nèi)容就移動側(cè)滑欄所占比例的剩余值
//val mainDx = dx / maxScrollLength * measuredWidth * (1 - mSidePercent)
//scrollBy(mainDx.toInt(), 0)
//側(cè)滑欄速度更大,這里據(jù)最大滑動距離和側(cè)滑欄的寬度做個映射
//val sideDx = dx / maxScrollLength * (measuredWidth * mSidePercent)
//側(cè)滑欄的移動不能使用scrollTo和scrollBy,因為僅僅移動的是其中的內(nèi)容,并不會移動整個view
//可以理解成scrollTo和scrollBy只是在該對象的原有位置移動,即使移動了也不會在其范圍之外顯示(draw)
//屬性動畫可以實現(xiàn)在父容器里面對子控件的移動,但是也是通過修改屬性值重新布局實現(xiàn)的
//sideView!!.scrollTo(sideView!!.scrollX + sideDx.toInt(), 0)
//這里累加mScrollLength后直接請求重新布局,在onLayout里面去處理移動
requestLayout()
}
}
private fun stopMove() {
//停止后,使用動畫移動到目標(biāo)位置
val terminalScrollX: Float = if (abs(mScrollLength) >= maxScrollLength / 2f) {
//觸發(fā)移動至完全展開,mScrollLength是個負(fù)數(shù)
-maxScrollLength
}else {
//如果移動沒過半應(yīng)該恢復(fù)狀態(tài),則恢復(fù)到原來狀態(tài)
0f
}
//這里使用ValueAnimator處理剩余的距離,模擬滑動到需要的位置
mAnimator = ValueAnimator.ofFloat(mScrollLength, terminalScrollX)
mAnimator!!.addUpdateListener { animation ->
mScrollLength = animation.animatedValue as Float
//請求重新布局
requestLayout()
}
//動畫結(jié)束時要更新狀態(tài)
mAnimator!!.addListener (onEnd = {
mState = if(mScrollLength == 0f) SLIDE_STATE_TYPE_CLOSED else SLIDE_STATE_TYPE_OPENED
})
//滑動動畫總時間應(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òu)造
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
//讀取XML參數(shù),設(shè)置相關(guān)屬性,這里有個很煩的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)
}
//重寫下面四個函數(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>
使用時在XML里面的例子,kotlin代碼幾乎不用寫了,注意命名空間是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>
主要問題
說起這個控件,問題可就很多了,當(dāng)然學(xué)到的東西也特別多,下面好好講講。
自定義XML中Fraction的使用
上一篇文章實際也用到了這個類型的屬性,即百分比,可是我沒測試下。在這個控件里面自己設(shè)置了一下,發(fā)現(xiàn)這個并不是像我想象填小數(shù)或者100內(nèi)的整數(shù),而是填完百分比后還要自己加一個百分號“%”!至于getFraction里面的base和pbase可以自己搜一下,我這就不展開講了,畢竟主要內(nèi)容是自定義view。
View提供的getDefaultSize
前面都是自己寫一個getSizeFromMeasureSpec函數(shù)來根據(jù)MeasureSpec模式獲得size,沒想到View中已經(jīng)提供了一個一模一樣的功能,尷尬了。
自定義LayoutParams
這個是這篇文章的重頭戲了,沒學(xué)習(xí)之前,我是萬萬沒想到一個View的LayoutParams屬性居然是父viewgroup的LayoutParams類型,而且自定義Viewgroup的同時還得自定義自身的LayoutParams,不然LayoutParams就一個height和一個width參數(shù)。話不多說,下面大致講講,詳細(xì)的還是找資料再補充下!
理解
關(guān)于一個View的LayoutParams屬性居然是父viewgroup的LayoutParams類型的描述,其實也很好理解,想想經(jīng)常用到的ConstraintLayout,我能不就是在它的子view中設(shè)置約束屬性么。所以我們要實現(xiàn)一個Layout,那子view不就是使用Layout的LayoutParams么。更何況哪面試經(jīng)常問的問題來說,一個view的寬高受什么影響,不就是父viewgroup的MeasureSpec和子view的LayoutParams決定的么,子view要對父view進(jìn)行約束,那不就得知道父view需要控制什么屬性么!
好了上面是我的理解,下面開始說明怎么使用。
LayoutParams需求
首先我們這里要實現(xiàn)一個類似官方側(cè)滑欄的功能,相信大家都用過DrawerLayout,在DrawerLayout里面我們通過指定一個子view的layout_gravity就能讓它成為側(cè)滑欄,沒錯,我們這也想實現(xiàn)這樣的效果。一開始我就直接寫嘛,app:layout_gravity不就是官方的么,可是我在XML中輸入這樣一個屬性,在onMeasure里面讀取不就可以判定了。結(jié)果代碼中的LayoutParams只有height和width兩個參數(shù),這麻煩了,找了下資料,原來要自己定義Viewgroup的LayoutParams!
自定義LayoutParams
這里就大致講下思路,代碼里面注釋寫的很清楚,分三步吧。第一步是要在代碼中創(chuàng)建一個自定義的LayoutParams,這里我就直接寫成內(nèi)部類了,實現(xiàn)其中幾個構(gòu)造函數(shù),并在構(gòu)造里面讀取到要用的參數(shù);第二步就是自定義參數(shù)了,需要創(chuàng)建一個xml文件來定義參數(shù),這里用到了枚舉類型的屬性,并且代碼里面也要定義好各種type,LayoutParams類中定義一個變量來儲存這個屬性;第三步就是重寫在布局文件被填充為對象的時候調(diào)用的幾個函數(shù),就大功告成了。
使用的時候要自己強制轉(zhuǎn)換一下,就能從子view的LayoutParams中拿到自定義的屬性了。
帶padding和margin的測量
側(cè)滑欄應(yīng)該占滿屏幕,不應(yīng)該帶padding和margin,另外測量就行,很簡單。主內(nèi)容部分我們要實現(xiàn)類似LinearLayout的效果,就得帶上帶padding和margin進(jìn)行測量。
這里用到了measureChildWithMargins這個函數(shù),他會接收child、MeasureSpec及寬高的使用情況對child進(jìn)行帶padding和margin的測量,可以點進(jìn)去看看這個函數(shù),里面又會調(diào)用getChildMeasureSpec去獲得child的MeasureSpec,根據(jù)MeasureSpec的三種類型及LayoutParams.layout_width/height的三種形式(確切值、wrap_content、match_parent),會產(chǎn)生九種不同的組合。
不過可以理解的是,控件如果設(shè)置了值那就是設(shè)置的值(三種情況);如果控件是match_parent,那EXACTLY和AT_MOST的值都會被該view用完(兩種情況),如果是UNSPECIFIED就要特殊處理了(一種情況);如果控件是wrap_content,在EXACTLY和AT_MOST里面,都會用給的值和AT_MOST生成一個新的MeasureSpec,并向下層傳遞下去,即wrap_content不知道要多大,但是知道最大有多大,下層的view按需索求(兩種情況),在UNSPECIFIED里也是特殊處理下(一種情況)。
這里還有個heightUsed要注意下,累加的高度應(yīng)該是父容器的padding,加上子控件的margin及高度共同構(gòu)成的。我這里只統(tǒng)計了高度,寬度上也是同理。在這里的setMeasuredDimension函數(shù)中,用的是整個控件最大的高度,而不是heightUsed,因為側(cè)滑欄占滿了控件的高度。但是如果我們僅僅是實現(xiàn)一個LinarLayout的話,就應(yīng)該用這個heightUsed了。
帶padding和margin的布局
這里和上面測量類似,要帶上父容器的padding和子控件的margin以及子控件的寬高進(jìn)行擺放。這里暫時不涉及動畫的話,就是要把各個child的left、top、right、bottom四個值計算清楚,同時注意curHeight的累加就行了。
側(cè)滑欄被主內(nèi)容里面控件覆蓋顯示問題
這里有個很奇怪的問題,就是側(cè)滑欄會被主內(nèi)容里面控件覆蓋顯示,側(cè)滑欄可以覆蓋主內(nèi)容的背景,但是主內(nèi)容里面的控件會在側(cè)滑欄上面繪制。這里我把側(cè)滑欄的view從XML第一個移到最后一個就沒事了,可是這不符合我們的邏輯,我又在onLayout里面最后去layout側(cè)滑欄,結(jié)果還是不行。后面想想繪制應(yīng)該是在draw里面吧,可能是直接for循環(huán)繪制的,我用iterator移除再添加到最后去不就行了,后面發(fā)現(xiàn)children的iterator并未提供刪除的功能,最后還是發(fā)現(xiàn)了removeViewInLayout和addViewInLayout兩個函數(shù),是專門在onLayout里面使用的,按前面的邏輯試一下,果然就好了。
設(shè)置padding被裁切的問題
這里如果在我們的TwoLayerSlideLayout上設(shè)置padding,那就會出現(xiàn)很神奇的效果,側(cè)滑欄也有padding了,但是仔細(xì)看,側(cè)滑欄的內(nèi)容位置是沒錯的,就是有padding的位置,側(cè)滑欄的內(nèi)容會被主內(nèi)容的背景覆蓋。查了下資料,又學(xué)了幾個東西,主要就是viewgroup的layoutParams里面有個clipToPadding屬性,默認(rèn)為true,會將padding部分的子view進(jìn)行裁切,我們在側(cè)滑欄layout前把它設(shè)置為false就行了。
滑動不生效問題
如果看了我前面的文章,在帶header和footer的滾動控件中,中間滾動的控件是TextView,也是無法移動,在那里我是通過設(shè)置clickable為true讓TextView也會消耗ACTION_DOWN事件,從而保證viewgroup能收到move事件。在寫當(dāng)前控件的時候,不僅是里面的TextView不會消耗ACTION_DOWN事件了,而且因為我們view還有很多是沒有子view的空隙,點擊在這些空隙里面同樣不會消耗ACTION_DOWN事件,導(dǎo)致事件序列被丟棄,ACTION_MOVE事件也沒了。
后面想想,好像還挺好解決的,之前沒思考光去考慮TextView了,如果子控件沒消耗耗ACTION_DOWN事件,事件會交到它的父控件的onTouchEvent處理,面試過的都知道,辦法補救在這里嗎?無論是子控件未消耗,還是點擊在空隙上,最終都會把ACTION_DOWN事件交到當(dāng)前控件的onTouchEvent方法內(nèi),我們在這里return true就可以了。
側(cè)滑欄的移動
前面幾篇文章都做過移動的處理了,這個view我開始也是照搬代碼,使用scrollBy去移動,側(cè)滑欄在主內(nèi)容移動的基礎(chǔ)上繼續(xù)通過scrollBy移動,結(jié)果想法很好,還計算了一系列值,最后發(fā)現(xiàn)只有主內(nèi)容會移動。實際想了想,我調(diào)用側(cè)滑欄的scrollBy去移動,移動的也只是側(cè)滑欄的內(nèi)容啊,也就是說移動是在側(cè)滑欄內(nèi)部進(jìn)行的,又繼續(xù)看了下滑動效果,果然側(cè)滑欄雖然沒有被scrollBy滑動覆蓋主內(nèi)容,但是側(cè)滑欄里面的內(nèi)容確實是以我設(shè)計的速度進(jìn)行的。
寫道這里我又想到了上面的clipToPadding屬性,viewgroup的layoutParams還有一個clipChildren屬性,就是不裁切不裁切孫view在父view超出的部分,可是就算側(cè)滑欄里面的控件移動到了主內(nèi)容上面,效果也還是不對的,因為側(cè)滑欄的背景并沒有移動,也就是說這是不可行的。
這里我想到了屬性動畫,屬性動畫是可以讓整個view移動的,但是在每一個move事件里面去創(chuàng)建一個屬性動畫,每次移動一小部分嗎?好像不太好,而且既然屬性動畫是根據(jù)屬性去修改位置的,我們直接去修改布局不就行了。這里據(jù)滑動值,計算出主內(nèi)容和側(cè)滑欄的偏移,然后使用requestLayout重新布局就可以了,布局的時候加上偏移,代碼很簡單。
滑動停止切換到目標(biāo)位置
這里和前面幾個view一樣,用ValueAnimator來模擬繼續(xù)滑動,但是上一篇文章中滾動選擇控件會因為動畫沒結(jié)束有繼續(xù)滑動導(dǎo)致出現(xiàn)滑出界的問題,這里解決下。主要就是增加了一個狀態(tài)的判定,分三個狀態(tài),如果動畫沒有結(jié)束,就點擊進(jìn)行滑動,在ACTION_DOWN事件時就把動畫停了,并移除結(jié)束監(jiān)聽回調(diào),這時候并不會修改mScrollLength,可以繼續(xù)交給新的滑動接管整個滑動過程,這樣用起來就流暢多了!
滑動速度問題
val mainOffset = -mScrollLength / maxScrollLength * measuredWidth * (1 - mSidePercent) val slideOffset = -mScrollLength / maxScrollLength * (measuredWidth * mSidePercent)
上面是我們主內(nèi)容和側(cè)滑欄偏移的計算代碼,邏輯是我們設(shè)定一個讓側(cè)滑欄展開的最大滑動距離,滑動的時候側(cè)滑欄按滑動距離占最大滑動距離的比例去展開側(cè)滑欄,也就是說滑動距離等于最大滑動距離時就展開了,中間按比例移動;對于主內(nèi)容,我們就讓它移動的最大距離為側(cè)滑欄所占屏幕寬度的剩余值,也就是說滑動距離等于最大滑動距離時主內(nèi)容就移動了側(cè)滑欄占屏幕寬度的剩余值,中間同樣時按比例移動。稍微理解下,很簡單,如果側(cè)滑欄占屏幕寬度的比例大于一半,那側(cè)滑欄速度就比主內(nèi)容大,反之主內(nèi)容速度大,實際上這樣也很合理!
到此這篇關(guān)于Android自定義view實現(xiàn)側(cè)滑欄詳解的文章就介紹到這了,更多相關(guān)Android側(cè)滑欄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android開發(fā)中g(shù)radle下載緩慢的問題級解決方法
本文介紹了解決Android開發(fā)中Gradle下載緩慢問題的幾種方法,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2025-02-02
Java4Android開發(fā)教程(二)hello world!
一般的開發(fā)教程都是介紹完安裝配置開發(fā)環(huán)境,緊接著來一篇hello world,算是國際慣例吧,我們當(dāng)然也不能免俗,哈哈,各位看官請看好了!2014-10-10
android studio與手機連接調(diào)試步驟詳解
這篇文章主要為大家詳細(xì)介紹了android studio與手機連接調(diào)試步驟,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07
Android Volley擴展實現(xiàn)支持進(jìn)度條的文件上傳功能
這篇文章主要為大家詳細(xì)介紹了Android Volley擴展實現(xiàn)文件上傳與下載功能,支持進(jìn)度條,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-12-12
Android 如何實現(xiàn)exclude aar包中的某個jar包
這篇文章主要介紹了Android 如何實現(xiàn)exclude aar包中的某個jar包,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android Studio error: Unable to start the daemon process的解決方
這篇文章主要介紹了在 Android Studio 上新建項目,出現(xiàn) Unable to start the daemon process問題的幾種的解決方法,需要的朋友可以參考下2020-10-10

