Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動控件_第1頁
Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動控件_第2頁
Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動控件_第3頁
Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動控件_第4頁
Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動控件_第5頁
已閱讀5頁,還剩7頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡介

第Android自定義view實(shí)現(xiàn)有header和footer作為layout使用的滾動控件目錄前言需求編寫代碼主要問題

前言

上兩篇文章對安卓自定義view的事件分發(fā)做了一些應(yīng)用,但是對于自定義view來講,并不僅僅是事件分發(fā)這么簡單,還有一個(gè)很重要的內(nèi)容就是view的繪制流程。接下來我這通過帶header和footer的Layout,來學(xué)習(xí)一下ViewGroup的自定義流程,并對其中的MeasureSpec、onMeasure以及onLayout加深理解。

需求

這里就是一個(gè)有header和footer的滾動控件,可以在XML中當(dāng)Layout使用,核心思想如下:

1、由header、XML內(nèi)容、footer三部分組成

2、滾動中間控件時(shí),上面有內(nèi)容時(shí)header不顯示,下面有內(nèi)容時(shí)footer不顯示

3、滑動到header和footer最大值時(shí)不能滑動,釋放的時(shí)候需要回彈

4、完全顯示時(shí)隱藏footer

編寫代碼

編寫代碼這部分還真讓我頭疼了一會,主要就是MeasureSpec的運(yùn)用,如何讓控件能夠超出給定的高度,如何獲得實(shí)際高度和控件高度,真是紙上得來終覺淺,絕知此事要躬行,看書那么多遍,實(shí)際叫自己寫起來真的費(fèi)勁,不過最終寫完,才真的敢說自己對measure和layout有一定了解了。

先看代碼,再講問題吧!

importandroid.annotation.SuppressLint

importandroid.content.Context

importandroid.graphics.Color

importandroid.util.AttributeSet

importandroid.util.TypedValue

importandroid.view.Gravity

importandroid.view.MotionEvent

importandroid.view.View

importandroid.view.ViewGroup

importandroid.widget.Scroller

importandroid.widget.TextView

importandroidx.core.view.forEach

importkotlin.math.min

*有header和footer的滾動控件

*核心思想:

*1、由header、container、footer三部分組成

*2、滾動中間控件時(shí),上面有內(nèi)容時(shí)header不顯示,下面有內(nèi)容時(shí)footer不顯示

*3、滑動到header和footer最大值時(shí)不能滑動,釋放的時(shí)候需要回彈

*4、完全顯示時(shí)隱藏footer

@SuppressLint("SetTextI18n","ViewConstructor")

classHeaderFooterView@JvmOverloadsconstructor(

context:Context,

attributeSet:AttributeSet=null,

defStyleAttr:Int=0,

varheader:View=null,

varfooter:View=null

):ViewGroup(context,attributeSet,defStyleAttr){

varonReachHeadListener:OnReachHeadListener=null

varonReachFootListener:OnReachFootListener=null

//上次事件的橫坐標(biāo)

privatevarmLastY=0f

//總高度

privatevartotalHeight=0

//是否全部顯示

privatevarisAllDisplay=false

//流暢滑動

privatevarmScroller=Scroller(context)

init{

//設(shè)置默認(rèn)的Header、Footer,這里是從構(gòu)造來的,如果外部設(shè)置需要另外處理

header=header:makeTextView(context,"Header")

footer=footer:makeTextView(context,"Footer")

//添加對應(yīng)控件

addView(header,0)

//這里還沒有加入XML中的控件

//Log.e("TAG","init:childCount=$childCount",)

addView(footer,1)

//創(chuàng)建默認(rèn)的Header\Footer

privatefunmakeTextView(context:Context,textStr:String):TextView{

returnTextView(context).apply{

layoutParams=LayoutParams(LayoutParams.MATCH_PARENT,dp2px(context,30f))

text=textStr

gravity=Gravity.CENTER

textSize=sp2px(context,13f).toFloat()

setBackgroundColor(Color.GRAY)

//不設(shè)置isClickable的話,點(diǎn)擊該TextView會導(dǎo)致mFirstTouchTarget為null,

//致使onInterceptTouchEvent不會被調(diào)用,只有ACTION_DOWN能被收到,其他事件都沒有

//因?yàn)槭录蛄兄蠥CTION_DOWN沒有被消耗(返回true),整個(gè)事件序列被丟棄了

//如果XML內(nèi)是TextView也會造成同樣情況,

isFocusable=true

isClickable=true

overridefunonMeasure(widthMeasureSpec:Int,heightMeasureSpec:Int){

super.onMeasure(widthMeasureSpec,heightMeasureSpec)

//父容器給當(dāng)前控件的寬高,默認(rèn)值盡量設(shè)大一點(diǎn)

valwidth=getSizeFromMeasureSpec(1080,widthMeasureSpec)

valheight=getSizeFromMeasureSpec(2160,heightMeasureSpec)

//對子控件進(jìn)行測量

forEach{child-

//寬度給定最大值

valchildWidthMeasureSpec=MeasureSpec.makeMeasureSpec(width,MeasureSpec.AT_MOST)

//高度不限定

valchildHeightMeasureSpec

=MeasureSpec.makeMeasureSpec(height,MeasureSpec.UNSPECIFIED)

//進(jìn)行測量,不測量的話measuredWidth和measuredHeight會為0

child.measure(childWidthMeasureSpec,childHeightMeasureSpec)

//Log.e("TAG","onMeasure:child.measuredWidth=${child.measuredWidth}")

//Log.e("TAG","onLayout:child.measuredHeight=${child.measuredHeight}")

//設(shè)置測量高度為父容器最大寬高

setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),

MeasureSpec.getSize(heightMeasureSpec))

privatefungetSizeFromMeasureSpec(defaultSize:Int,measureSpec:Int):Int{

//獲取MeasureSpec內(nèi)模式和尺寸

valmod=MeasureSpec.getMode(measureSpec)

valsize=MeasureSpec.getSize(measureSpec)

returnwhen(mod){

MeasureSpec.EXACTLY-size

MeasureSpec.AT_MOST-min(defaultSize,size)

else-defaultSize//MeasureSpec.UNSPECIFIED

overridefunonLayout(changed:Boolean,left:Int,top:Int,right:Int,bottom:Int){

varcurHeight=0

//Log.e("TAG","onLayout:childCount=${childCount}")

forEach{child-

//footer最后處理

if(indexOfChild(child)!=1){

//Log.e("TAG","onLayout:child.measuredHeight=${child.measuredHeight}")

child.layout(left,top+curHeight,right,

top+curHeight+child.measuredHeight)

curHeight+=child.measuredHeight

//處理footer

valfooter=getChildAt(1)

//完全顯示內(nèi)容時(shí)不加載footer,header不算入內(nèi)容

if(measuredHeightcurHeight-header!!.height){

//設(shè)置全部顯示flag

isAllDisplay=false

footer.layout(left,top+curHeight,right,top+curHeight+footer.measuredHeight)

curHeight+=footer.measuredHeight

//布局完成,滾動一段距離,隱藏header

scrollBy(0,header!!.height)

//設(shè)置總高度

totalHeight=curHeight

overridefunonInterceptTouchEvent(ev:MotionEvent):Boolean{

//Log.e("TAG","onInterceptTouchEvent:ev=$ev")

ev.let{

when(ev.action){

MotionEvent.ACTION_DOWN-mLastY=ev.y

MotionEvent.ACTION_MOVE-returntrue

returnsuper.onInterceptTouchEvent(ev)

@SuppressLint("ClickableViewAccessibility")

overridefunonTouchEvent(ev:MotionEvent):Boolean{

//Log.e("TAG","onTouchEvent:height=$height,measuredHeight=$measuredHeight")

ev.let{

when(ev.action){

MotionEvent.ACTION_MOVE-moveView(ev)

MotionEvent.ACTION_UP-stopMove()

returnsuper.onTouchEvent(ev)

privatefunmoveView(e:MotionEvent){

//Log.e("TAG","moveView:height=$height,measuredHeight=$measuredHeight")

valdy=mLastY-e.y

//更新點(diǎn)擊的縱坐標(biāo)

mLastY=e.y

//縱坐標(biāo)的可滑動范圍,0到隱藏部分高度,全部顯示內(nèi)容時(shí)是header高度

valscrollMax=if(isAllDisplay){

header!!.height

}else{

totalHeight-height

//限定滾動范圍

if((scrollY+dy)=scrollMax(scrollY+dy)=0){

//觸發(fā)移動

scrollBy(0,dy.toInt())

privatefunstopMove(){

//Log.e("TAG","stopMove:height=$height,measuredHeight=$measuredHeight")

//如果滑動到顯示了header,就通過動畫隱藏header,并觸發(fā)到達(dá)頂部回調(diào)

if(scrollYheader!!.height){

mScroller.startScroll(0,scrollY,0,header!!.height-scrollY)

onReachHeadListener.onReachHead()

}elseif(!isAllDisplayscrollY(totalHeight-height-footer!!.height)){

//如果滑動到顯示了footer,就通過動畫隱藏footer,并觸發(fā)到達(dá)底部回調(diào)

mScroller.startScroll(0,scrollY,0,

(totalHeight-height-footer!!.height)-scrollY)

onReachFootListener.onReachFoot()

invalidate()

//流暢地滑動

overridefuncomputeScroll(){

if(mSputeScrollOffset()){

scrollTo(mScroller.currX,mScroller.currY)

postInvalidate()

//單位轉(zhuǎn)換

@Suppress("SameParameterValue")

privatefundp2px(context:Context,dpVal:Float):Int{

returnTypedValue.applyDimension(

TypedValue.COMPLEX_UNIT_DIP,dpVal,context.resources

.displayMetrics

).toInt()

@Suppress("SameParameterValue")

privatefunsp2px(context:Context,spVal:Float):Int{

valfontScale=context.resources.displayMetrics.scaledDensity

return(spVal*fontScale+0.5f).toInt()

interfaceOnReachHeadListener{

funonReachHead()

interfaceOnReachFootListener{

funonReachFoot()

}

主要問題

父容器給當(dāng)前控件的寬高

這里就是MeasureSpec的理解了,onMeasure中給了兩個(gè)參數(shù):widthMeasureSpec和heightMeasureSpec,里面包含了父控件給當(dāng)前控件的寬高,根據(jù)模式的不同可以取出給的數(shù)值,根據(jù)需要設(shè)定自身的寬高,需要注意setMeasuredDimension函數(shù)設(shè)定后,measuredWidth和measuredHeight才有值。

對子控件進(jìn)行測量

這里很容易忽略的是,當(dāng)繼承viewgroup的時(shí)候,我們要手動去調(diào)用child的measure函數(shù),去測量child的寬高。一開始我也沒注意到,當(dāng)我繼承LineaLayout的時(shí)候是沒問題的,后面改成viewgroup后就出問題了,看了下LineaLayout的源碼,里面的onMeasure函數(shù)中實(shí)現(xiàn)了對child的測量。

對子控件的測量時(shí),MeasureSpec又有用了,比如說我們希望XML中的內(nèi)容不限高度或者高度很大,這時(shí)候MeasureSpec.UNSPECIFIED就有用了,而寬度我們希望最大就是控件寬度,就可以給個(gè)MeasureSpec.AT_MOST,注意我們給子控件的MeasureSpec也是有兩部分的,需要通過makeMeasureSpec創(chuàng)建。

子控件的擺放

由于我們的footer和header是在構(gòu)造里面創(chuàng)建并添加到控件中的,這時(shí)候XML內(nèi)的view還沒加進(jìn)來,所以需要注意下footer實(shí)際在控件中是第二個(gè),擺放的時(shí)候根據(jù)index要特殊處理一下。

其他控件我們根據(jù)左上右下的順序擺放就行了,注意onMeasure總對子控件measure了才有寬高。

控件總高度和控件高度

因?yàn)樾枨?,我們的控件要求是中間可以滾動,所以在onMeasure總,我們用到了MeasureSpec.UNSPECIFIED,這時(shí)候控件的高度和實(shí)際總高度就不一致了。這里我們需要在onLayout中累加到來,實(shí)際擺放控件的時(shí)候也要用到這個(gè)高度,順勢而為了。

header和footer的初始化顯示與隱藏

這里希望在開始的時(shí)候隱藏header,所以需要在onLayout完了的時(shí)候,向上滾動控件,高度為header的高度。

根據(jù)需求,完全顯示內(nèi)容的時(shí)候,我們不希望顯示footer,這里也要在onLayout里面實(shí)現(xiàn),根據(jù)XML內(nèi)容的高度和控件高度一比較就知道需不需要layoutfooter了。

header和footer的動態(tài)顯示與隱藏

這里就和前面兩篇文章類似了,就是在縱坐標(biāo)上滾動控件,限定滾動范圍,在ACTION_UP事件時(shí)判定滾動后的狀態(tài),動態(tài)去顯示和隱藏header和footer,思路很明確,邏輯可能復(fù)雜一點(diǎn)。

使用

這里簡單說下使用吧,就是作為Layout,中間可以放控件,中間控件可以指定特別大的高度,也可以wrap_content,但是內(nèi)容很高。

xmlversion="1.0"encoding="utf-8"

androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="/apk/res/android"

xmlns:app="/apk/res-auto"

xmlns:tools="/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity"

com.silencefly96.module_common.view.HeaderFooterView

android:id="@+id/hhView"

android:layout_width="match_parent"

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論