前言
做Android幾年,到現(xiàn)在,突然感覺寫東西的效率提高很多,能寫的東西也越來(lái)越多,突然就有種,忙不過(guò)來(lái)的感覺,既興奮,有時(shí)候又會(huì)感覺有些累了.
視圖控件是一類控件,并不單選電影選座的.這只是其中最具有代表性的一個(gè)而矣.它們具有一個(gè)特性,繪制面積非常大,繪制元素往往很密集.需要全方位的滾動(dòng),可以縮放,等等.我們這次帶著一種不一樣的思路,來(lái)做一個(gè)真正強(qiáng)大的此類基本視圖控件.
效果預(yù)覽
HierarchyView演示
![]()
HierarchyView演示
![]()
下載示例
這里介紹一下當(dāng)前大部分此類控件的弊端
- 往往為純繪制,擴(kuò)展性極差
- 因?yàn)槭褂肕atrix作縮放滾動(dòng),所以丟失了控件己有的fling滾動(dòng)效果.在矩陣面積較大時(shí),體驗(yàn)不好
- 做一些效果很難,如點(diǎn)擊一類.
本項(xiàng)目使用核心技術(shù)
- 控件繪制
- 控件排版
- 控件復(fù)用理解
- Canvas繪圖
本項(xiàng)目達(dá)成目標(biāo)
- 采用控件己有特性如滾動(dòng),慣性滾動(dòng)
- 采用類子控件排版并繪制,控制性好,使用如ListView/RecyclerView一般
- 保留了控件所有操作,如點(diǎn)擊效果,點(diǎn)擊等.
- 核心原理簡(jiǎn)單.擴(kuò)展性強(qiáng).是一套可大量并快速?gòu)?fù)用此類需求和基礎(chǔ)性控件
原理講解(Kotlin)
基本原理1:仿制ViewGroup控件,因?yàn)閂iewGroup強(qiáng)制的測(cè)量,排版,以及繪制,我們無(wú)法控制,所以在此,我們需要模擬一個(gè)ViewGroup,實(shí)現(xiàn)子控件測(cè)量,排版,以及繪制
Step1 添加100個(gè)簡(jiǎn)單控件
示例為:HierarchyLayout1
本控件為一個(gè)繼承了View的子控件,非ViewGroup,初始添加100個(gè)子控件,此添加為添加到內(nèi)部維護(hù)的集合內(nèi)
init {
val random=Random()
(0..100).forEach {
val view=View(context)
val color=Color.argb(0xff,random.nextInt(0xFF),random.nextInt(0xFF),random.nextInt(0xFF))
val pressColor=Color.argb(0xff,Math.min(0xff,Color.red(color)+30),Math.min(0xff,Color.green(color)+30),Math.min(0xff,Color.blue(color)+30))
val drawable=StateListDrawable()
drawable.addState(intArrayOf(android.R.attr.state_empty),ColorDrawable(color))
drawable.addState(intArrayOf(android.R.attr.state_pressed),ColorDrawable(pressColor))
view.backgroundDrawable=drawable
view.setOnClickListener {
Toast.makeText(context,"點(diǎn)擊${indexOfChild(it)}",Toast.LENGTH_SHORT).show()
}
//本控件實(shí)現(xiàn)ViewManager方法,所以有addView,而非ViewGroup添加
addView(view,ViewGroup.LayoutParams(300,300))
}
}
Step2 控件模擬測(cè)量
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
for(view in views){
measureChildWithMargins(view,MeasureSpec.getMode(widthMeasureSpec),MeasureSpec.getMode(heightMeasureSpec))
}
}
fun measureChildWithMargins(child: View, widthMode: Int, heightMode: Int) {
val lp = child.layoutParams as ViewGroup.LayoutParams
val widthSpec = getChildMeasureSpec(width, widthMode, paddingLeft + paddingRight, lp.width)
val heightSpec = getChildMeasureSpec(height, heightMode, paddingTop + paddingBottom, lp.height)
child.measure(widthSpec, heightSpec)
}
fun getChildMeasureSpec(parentSize: Int, parentMode: Int, padding: Int, childDimension: Int): Int {
val size = Math.max(0, parentSize - padding)
var resultSize = 0
var resultMode = 0
if (childDimension >= 0) {
resultSize = childDimension
resultMode = View.MeasureSpec.EXACTLY
} else {
if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
resultSize = size
resultMode = parentMode
} else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultSize = size
if (parentMode == View.MeasureSpec.AT_MOST || parentMode == View.MeasureSpec.EXACTLY) {
resultMode = View.MeasureSpec.AT_MOST
} else {
resultMode = View.MeasureSpec.UNSPECIFIED
}
}
}
return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode)
}
Step3 模擬排版
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val value=8
(0..getChildCount()-1).forEach {
val row=(it/value)
val column=it%value
val childView=getChildAt(it)
debugLog("onLayout index:$it row:$row column:$column")
childView.layout((column*300), (row*300), ((column+1)*300), ((row+1)*300))
setChildPress(childView,false)
}
}
Step4 繪制控件
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val value=8
(0..getChildCount()-1).forEach {
val row=(it/value)
val column=it%value
val childView=getChildAt(it)
canvas.save()
canvas.translate((column*300).toFloat(), (row*300).toFloat())
childView.draw(canvas)
canvas.restore()
}
}
Step5 完成控件縮放控制
實(shí)現(xiàn)ScaleGestureDetector對(duì)象,完成縮放示例,
private var MAX_SCALE=3.0f
private var MIN_SCALE=1f
override fun onScale(detector: ScaleGestureDetector): Boolean {
var scaleFactor=detector.scaleFactor
val matrixScaleX = getMatrixScaleX()
val matrixScaleY = getMatrixScaleY()
if(MIN_SCALE>scaleFactor*matrixScaleX){
scaleFactor=MIN_SCALE/matrixScaleX
} else if(MAX_SCALE<scaleFactor*matrixScaleX){
scaleFactor=MAX_SCALE/matrixScaleX
}
scaleMatrix.postScale(scaleFactor, scaleFactor, detector.focusX, detector.focusY)
//計(jì)算出放大中心點(diǎn)
val scrollX=((scrollX+detector.focusX)/matrixScaleX*getMatrixScaleX())
val scrollY=((scrollY+detector.focusY)/matrixScaleY*getMatrixScaleY())
//動(dòng)態(tài)滾動(dòng)至縮放中心點(diǎn)
scrollTo(((scrollX-detector.focusX)).toInt(), ((scrollY-detector.focusY)).toInt())
ViewCompat.postInvalidateOnAnimation(this)
return true
}
以上,完成了對(duì)基本原理的理解,這是區(qū)別通過(guò)純繪制的最大區(qū)別.保留了控件的所有特性,所以可以通過(guò)布局初始化控件,設(shè)置點(diǎn)擊,減少大量的繪制控制邏輯,
接下來(lái)正式開始控件
Step1設(shè)計(jì)數(shù)據(jù)適配器
abstract class SeatTableAdapter(val table: SeatTable1){
/**
* 獲得頂部座位
*/
abstract fun getHeaderSeatLayout(parent:ViewGroup):View
/**
* 獲得屏幕控件
*/
abstract fun getHeaderScreenView(parent:ViewGroup):View
/**
* 獲得座位排左側(cè)指示控件
*/
abstract fun getSeatNumberView(parent:ViewGroup):View
/**
* 綁定座位序列
*/
open fun bindSeatNumberView(view:View,row:Int)=Unit
/**
* 綁定序號(hào)列數(shù)據(jù)
*/
open fun bindNumberLayout(numberLayout:ViewGroup)=Unit
/**
* 獲得座位號(hào)
*/
abstract fun getSeatView(parent:ViewGroup,row:Int,column:Int):View
/**
* 綁定座位數(shù)據(jù)
*/
abstract fun bindSeatView(parent:ViewGroup,view:View,row:Int,column:Int)
/**
* 獲得座位列數(shù)
*/
abstract fun getSeatColumnCount():Int
/**
* 獲得座位排數(shù)
*/
abstract fun getSeatRowCount():Int
/**
* 獲得橫向多余空間
*/
abstract fun getHorizontalSpacing(column:Int):Int
/**
* 獲得縱向多余空間
*/
abstract fun getVerticalSpacing(row:Int):Int
/**
* 某個(gè)座位是否可見
*/
open fun isSeatVisible(row:Int,column:Int)=true
/**
* 獲得當(dāng)前座位節(jié)點(diǎn)信息
*/
fun getSeatNodeItem(row:Int,column:Int)=table.seatArray[row][column]
/**
* 選中一個(gè)條目
*/
fun setItemSelected(row:Int,column:Int,select:Boolean){
table.setItemSelected(row,column,select)
}
fun setItemSelected(item:SeatNodeInfo,select:Boolean){
table.setItemSelected(item,select)
}
fun getSeatNodeByView(v:View)=table.getSeatNodeByView(v)
}
Step2初始化信息
以一個(gè)對(duì)象,初始化記錄所有座位的節(jié)點(diǎn)信息,排版位置,行,列(第一版時(shí)做法)等,放在一個(gè)二維數(shù)組內(nèi).方便快速索引,然后測(cè)量所有基礎(chǔ)控件
/**
* 設(shè)置數(shù)據(jù)適配器
*/
fun setAdapter(newAdapter: SeatTableAdapter){
//重置table
resetSeatTable()
adapter= newAdapter
//屏幕附加信息
seatLayout = newAdapter.getHeaderSeatLayout(parent as ViewGroup)
//屏幕布局
screenView = newAdapter.getHeaderScreenView(parent as ViewGroup)
//執(zhí)行計(jì)算,獲得矩陣前信息/屏幕信息/座位以及整個(gè)影院大小信息
val columnCount = newAdapter.getSeatColumnCount()
val rowCount = newAdapter.getSeatRowCount()
seatArray = Array(rowCount){ row->
//添加序列信息
val numberView=newAdapter.getSeatNumberView(parent as ViewGroup)
newAdapter.bindSeatNumberView(numberView,row)
numberLayout.addView(numberView)
//添加節(jié)點(diǎn)信息
(0..columnCount-1).map {SeatNodeInfo(row,it) }.toTypedArray()
}
val seatView = recyclerBin.newViewWithMeasured(seatArray[0][0])
newAdapter.bindSeatView(parent as ViewGroup,seatView,0,0)
addView(seatView)
newAdapter.bindNumberLayout(numberLayout)
requestLayout()
}
Step3在滾動(dòng)時(shí)建立回收與復(fù)用機(jī)制
- 復(fù)用原理為:界面發(fā)生滾動(dòng)時(shí),獲得當(dāng)前屏幕矩陣位置:screenRect.set(scrollX, scrollY, scrollX + width, scrollY + height)
- 清空所有集合內(nèi)添加控件到緩存,等待被使用
- 快速索引到當(dāng)前橫/縱向(第二版己優(yōu)化),然后遍歷并刷新所有數(shù)據(jù)(這里做法非常合理,效率很高,不能通過(guò)tag復(fù)用,因?yàn)樾枰檎?性能就低,直接清洗,再使用,效率最高)
//起始縱向矩陣
val startRange=findScreenRange(seatArray.map { it[0] }.toTypedArray()){
tmpRect.set((it.left * matrixScaleX).toInt(),(it.top * matrixScaleY).toInt(),(it.right * matrixScaleX).toInt(), (it.bottom * matrixScaleY).toInt())
intersetsVerticalRect(screenRect,tmpRect)
}
//橫向查
val endRange=findScreenRange(seatArray[0]){
tmpRect.set((it.left * matrixScaleX).toInt(),(it.top * matrixScaleY).toInt(),(it.right * matrixScaleX).toInt(), (it.bottom * matrixScaleY).toInt())
intersetsHorizontalRect(screenRect,tmpRect)
}
/**
* 查找屏幕內(nèi)起始計(jì)算矩陣,因?yàn)楫?dāng)數(shù)據(jù)量非常大時(shí),不快速找到起始遍歷位置,會(huì)非常慢
*/
private fun findScreenRange(array:Array<SeatNodeInfo>,predicate:(Rect)->Boolean):IntRange{
var (start,end)=-1 to -1
//縱向查
run{ array.forEachIndexed { row,node ->
val intersects=predicate(node.layoutRect)
if(-1==start&&intersects){
start=row//記錄頭
} else if(-1!=start&&!intersects){
end=row
return@run
}
}
}
//檢測(cè)最后結(jié)果
if(-1==end){
end=array.size-1
}
return IntRange(start,end)
}
- 繪制所有元素
//遍歷所有子孩子
fun forEachChild(action:(View)->Unit)=views.forEach(action)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
adapter?:return
val st=System.currentTimeMillis()
//當(dāng)前屏幕所占矩陣
val matrixScaleX = getMatrixScaleX()
val matrixScaleY = getMatrixScaleY()
//繪制座位整體信息
screenRect.set(scrollX, scrollY, scrollX + width, scrollY + height)
//繪電影院座位
forEachChild { drawSeatView(canvas, it, matrixScaleX, matrixScaleY) }
//繪屏幕
drawScreen(canvas, screenRect, matrixScaleX, matrixScaleY)
//繪左側(cè)指示器
drawNumberIndicator(canvas, matrixScaleX, matrixScaleY)
//繪當(dāng)前座位描述
drawSeatLayout(canvas)
//繪縮略圖
drawPreView(canvas)
debugLog("onDraw:${System.currentTimeMillis()-st}")
}
/**
* 繪制當(dāng)前屏幕內(nèi)座位
*/
private fun drawSeatView(canvas: Canvas,childView:View, matrixScaleX: Float, matrixScaleY: Float) {
canvas.save()
//此處,按此比例放大控件
canvas.scale(matrixScaleX, matrixScaleY)
canvas.translate(childView.left.toFloat(), childView.top.toFloat())
val item=childView.tag as SeatNodeInfo
childView.isSelected=item.select
childView.draw(canvas)
canvas.restore()
}
以上,完成了所有核心說(shuō)明
以模擬ViewGroup,復(fù)用View,繪制的另一種思想,做此類視圖,體驗(yàn)與性能并存,第二版專為優(yōu)化性能,做到百億以上,無(wú)壓力運(yùn)算.本項(xiàng)目是以HierarchyLayout為核心開發(fā)完后,花4小時(shí),就寫出核心,然后優(yōu)化而成,所以讀懂核心 ,此類控件以后就非常簡(jiǎn)單了.并且第二版對(duì)二維運(yùn)算的簡(jiǎn)化,有更多可參考地方.以上,非常感謝閱讀!
`