Preface
The previous two articles have made some applications for event distribution of Android custom view:Android custom view implements left-slide RecyclerView Detailed explanation、Android custom view implements left swipe within list to delete Item, but for a custom view, it is not just event distribution, but another very important content is the drawing process of the view. Next, I will learn the custom process of ViewGroup through the Layout with header and footer, and deepen my understanding of MeasureSpec, onMeasure and onLayout.
need
Here is a scroll control with header and footer, which can be used in XML as Layout. The core idea is as follows:
1. It consists of three parts: header, XML content, and footer
2. When scrolling the intermediate control, the header does not display when there is content on it, and the footer does not display when there is content on it.
3. When sliding to the maximum value of the header and footer, it is necessary to rebound when releasing.
4. Hide footer when fully displayed
Writing code
Writing the code part really gave me a headache for a while. The main thing is the use of MeasureSpec, how to make the control exceed a given height, how to obtain the actual height and the height of the control, it is really shallow to learn from it. You must practice it by reading the book so many times, but in fact, it is really hard to write it yourself. However, after finally writing it, I really dare to say that I have a certain understanding of measure and layout.
Let’s look at the code first and then talk about the problem!
import import import import import import import import import import import import import /** * Scrolling controls with header and footer * Core idea: * 1. It consists of three parts: header, container and footer * 2. When scrolling the intermediate control, the header does not display when there is content on it, and the footer does not display when there is content on it. * 3. It cannot slide when sliding to the maximum value of the header and footer, and it needs to rebound when released. * 4. Hide footer when fully displayed */ @SuppressLint("SetTextI18n", "ViewConstructor") class HeaderFooterView @JvmOverloads constructor( context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = 0, var header: View? = null, var footer: View? = null ): ViewGroup(context, attributeSet, defStyleAttr){ var onReachHeadListener: OnReachHeadListener? = null var onReachFootListener: OnReachFootListener? = null //The horizontal axis of the last event private var mLastY = 0f //Total height private var totalHeight = 0 // Whether to display all private var isAllDisplay = false //Smooth sliding private var mScroller = Scroller(context) init { //Set the default header and Footer, which is from the construction. If external settings need to be processed separately header = header ?: makeTextView(context, "Header") footer = footer ?: makeTextView(context, "Footer") //Add the corresponding control addView(header, 0) //The controls in XML have not been added here //("TAG", "init: childCount=$childCount", ) addView(footer, 1) } //Create the default header\Footer private fun makeTextView(context: Context, textStr: String): TextView { return TextView(context).apply { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, dp2px(context, 30f)) text = textStr gravity = textSize = sp2px(context, 13f).toFloat() setBackgroundColor() //If isClickable is not set, clicking on the TextView will cause mFirstTouchTarget to be null. //Successively, onInterceptTouchEvent will not be called, only ACTION_DOWN can be received, and no other events can be received //Because ACTION_DOWN in the event sequence is not consumed (return true), the entire event sequence is discarded //The same situation will also occur if the XML is a TextView. isFocusable = true isClickable = true } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { (widthMeasureSpec, heightMeasureSpec) //The parent container gives the current control width and height, and the default value should be larger as much as possible val width = getSizeFromMeasureSpec(1080, widthMeasureSpec) val height = getSizeFromMeasureSpec(2160, heightMeasureSpec) //Measure the subcontrol forEach { child -> //The maximum value of width is given val childWidthMeasureSpec = (width, MeasureSpec.AT_MOST) //The height is not limited val childHeightMeasureSpec = (height, ) //Please measure. If you do not measure, measureWidth and measuredHeight will be 0 (childWidthMeasureSpec, childHeightMeasureSpec) //("TAG", "onMeasure: =${}") //("TAG", "onLayout: =${}") } //Set the measurement height to the maximum width and height of the parent container setMeasuredDimension((widthMeasureSpec), (heightMeasureSpec)) } private fun getSizeFromMeasureSpec(defaultSize: Int, measureSpec: Int): Int { //Get the mode and size in MeasureSpec val mod = (measureSpec) val size = (measureSpec) return when (mod) { -> size MeasureSpec.AT_MOST -> min(defaultSize, size) else -> defaultSize // } } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { var curHeight = 0 //("TAG", "onLayout: childCount=${childCount}") forEach { child -> //Footer final processing if (indexOfChild(child) != 1) { //("TAG", "onLayout: =${}") (left, top + curHeight, right, top + curHeight + ) curHeight += } } //Process footer val footer = getChildAt(1) //The footer is not loaded when the content is fully displayed, and the header is not included in the content if (measuredHeight < curHeight - header!!.height) { //Set all flags to display isAllDisplay = false (left, top + curHeight, right,top + curHeight + ) curHeight += } //The layout is completed, scroll for a distance, hide the header scrollBy(0, header!!.height) //Set the total height totalHeight = curHeight } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { //("TAG", "onInterceptTouchEvent: ev=$ev") ev?.let { when() { MotionEvent.ACTION_DOWN -> mLastY = MotionEvent.ACTION_MOVE -> return true } } return (ev) } @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(ev: MotionEvent?): Boolean { //("TAG", "onTouchEvent: height=$height, measuredHeight=$measuredHeight") ev?.let { when() { MotionEvent.ACTION_MOVE -> moveView(ev) MotionEvent.ACTION_UP -> stopMove() } } return (ev) } private fun moveView(e: MotionEvent) { //("TAG", "moveView: height=$height, measuredHeight=$measuredHeight") val dy = mLastY - //Update the vertical coordinate of the click mLastY = //The sliding range of the vertical coordinate, 0 to hide part of the height, the header height is when all the content is displayed val scrollMax = if (isAllDisplay) { header!!.height }else { totalHeight - height } //Limited scrolling range if ((scrollY + dy) <= scrollMax && (scrollY + dy) >= 0) { //Trigger the movement scrollBy(0, ()) } } private fun stopMove() { //("TAG", "stopMove: height=$height, measuredHeight=$measuredHeight") //If you slide to the display header, hide the header through animation and trigger the callback to the top if (scrollY < header!!.height) { (0, scrollY, 0, header!!.height - scrollY) onReachHeadListener?.onReachHead() }else if(!isAllDisplay && scrollY > (totalHeight - height - footer!!.height)) { //If you slide to the footer displayed, hide the footer through animation and trigger the callback to the bottom (0, scrollY,0, (totalHeight - height- footer!!.height) - scrollY) onReachFootListener?.onReachFoot() } invalidate() } //Swipe smoothly override fun computeScroll() { if (()) { scrollTo(, ) postInvalidate() } } //Unit conversion @Suppress("SameParameterValue") private fun dp2px(context: Context, dpVal: Float): Int { return ( TypedValue.COMPLEX_UNIT_DIP, dpVal, .displayMetrics ).toInt() } @Suppress("SameParameterValue") private fun sp2px(context: Context, spVal: Float): Int { val fontScale = return (spVal * fontScale + 0.5f).toInt() } interface OnReachHeadListener{ fun onReachHead() } interface OnReachFootListener{ fun onReachFoot() } }
Main issues
The width and height of the parent container to the current control
This is the understanding of MeasureSpec. OnMeasure is given two parameters: widthMeasureSpec and heightMeasureSpec, which contains the width and height of the parent control to the current control. The given value can be taken according to the different modes and set its own width and height according to needs. You need to pay attention to the value after the setMeasuredDimension function is set, and only measuredWidth and measuredHeight have values.
Measure the subcontrol
It is easy to ignore here that when inheriting the viewgroup, we have to manually call the child's measure function to measure the width and height of child. At first I didn't notice that when I inherited LineaLayout, there was no problem. After changing it to viewgroup, there was a problem. I looked at the source code of LineaLayout, and the measurement of child is implemented in the onMeasure function inside.
When measuring child controls, MeasureSpec is useful again. For example, we hope that the content in XML is not limited to height or is very high, which is useful at this time. We hope that the maximum width is the control width, so we can give MeasureSpec.AT_MOST. Note that the MeasureSpec we give child controls also has two parts, and it needs to be created through makeMeasureSpec.
Placement of sub-controls
Since our footer and header are created and added to the control in the construct, the view in the XML has not been added yet, so you need to note that footer is actually the second in the control, and it must be handled specially according to the index when placing it.
We just place other controls in the order of the upper left and lower right. Note that the onMeasure always measures the width and height of the child control.
Total control height and control height
Because of the requirement, our control requires that it can scroll in the middle, so we use it in the onMeasure total, and the height of the control is inconsistent with the actual total height. Here we need to accumulate in onLayout, and we also need to use this height when actually placing controls, so as to follow the trend.
Initialization of header and footer shows and hides
Here I hope to hide the header at the beginning, so when onLayout is finished, scroll up the control, the height is the height of the header.
According to the requirements, when fully displaying the content, we do not want to display the footer. We also need to implement it in onLayout. According to the height of the XML content and the height of the control, we will know whether the layout footer is needed.
Dynamic display and hiding of header and footer
This is similar to the previous two articles, which is to scroll the controls on the vertical coordinates, limit the scrolling range, determine the state after scrolling during the ACTION_UP event, and dynamically display and hide the header and footer. The idea is very clear, and the logic may be more complicated.
use
Let’s briefly talk about using it here. It means that as a Layout, you can place a control in the middle, and the intermediate control can specify a particularly large height, or you can wrap_content, but the content is very high.
<?xml version="1.0" encoding="utf-8"?> < xmlns:andro 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. android: android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/teal_700" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> <TextView android:text="@string/test_string" android:focusable="true" android:clickable="true" android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.silencefly96.module_common.> </>
The test_string here is very long. When scrolling, the header and footer can be pulled out, and when released, it will shrink back. You can also get callbacks for adding bottoming and topping in the controls in the code.
The ACTION_MOVE event is not triggered when the middle is TextView
In the above XML layout, if clickable=true is not added, only an ACTION_DOWN event will be received in the control, and then there will be no more, even in the dispatchTouchEvent. After investigation, if isClickable is not set, clicking on the TextView will cause mFirstTouchTarget to be null, causing the onInterceptTouchEvent to not be called, because ACTION_DOWN in the event sequence is not consumed (returned true), and the entire event sequence is discarded.
Conclusion
In fact, this control is not written very well, and it is still not good to use it, but you can still understand a lot of things if you use it for learning.
This is the article about Android custom view implementation of Layout with header and footer. For more related Layout content with header and footer, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!