




版權(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)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2026屆湖北省八校聯(lián)合體化學(xué)高二第一學(xué)期期中考試試題含解析
- 2025年城鄉(xiāng)教育資源均衡配置項(xiàng)目風(fēng)險(xiǎn)評估報(bào)告
- 2025年醫(yī)院三基三嚴(yán)考試題及答案
- 2025年急危重癥理論知識及急救技能考試題題庫及答案
- 2025年安全生產(chǎn)試題及答案
- 2026屆上海市北中學(xué)化學(xué)高一上期中聯(lián)考試題含解析
- 2025年環(huán)保產(chǎn)業(yè)技術(shù)創(chuàng)新應(yīng)用案例與產(chǎn)業(yè)升級路徑研究報(bào)告
- 江西中考景德鎮(zhèn)數(shù)學(xué)試卷
- 井岡山市初中數(shù)學(xué)試卷
- 心內(nèi)科繼續(xù)教育護(hù)理課件
- 河北省專業(yè)技術(shù)職務(wù)任職資格申報(bào)評審條件摘編(2003年版)
- GB/T 4666-2009紡織品織物長度和幅寬的測定
- GB/T 15166.4-2021高壓交流熔斷器第4部分:并聯(lián)電容器外保護(hù)用熔斷器
- GB/T 13477.17-2017建筑密封材料試驗(yàn)方法第17部分:彈性恢復(fù)率的測定
- 送達(dá)地址確認(rèn)書(完整版)
- 四川滑雪場商業(yè)綜合體設(shè)計(jì)方案文本含個(gè)方案 知名設(shè)計(jì)院
- 日立電梯常用零配件價(jià)格清單
- 單位人事證明(共7篇)
- 水泵設(shè)備單機(jī)試運(yùn)轉(zhuǎn)記錄
- 保密管理-公司涉密人員保密自查表
- 日常安全檢查記錄
評論
0/150
提交評論