Window, View, View drawing process, custom View, custom attributes

## Prepare for
the creation process of the view in the Knowledge Activity

Through this article, we briefly summarize,

The root View held inside the Window bound by the Activity Context Context is DecorView, which is a top-level view that contains all views in a page, including the status bar, title bar, and view label reflection in the layout in Acitivity Create view objects.

The view tag in the layout in Acitivity is through
LayoutInflater.from(mContext).inflate(resId, contentParent).
Reflection creates various view objects and puts them in the parent container contentParent. This parent container contentParent is actually
a view with an id called android.R.id.content in the sub-view subdecor of DecorView
.

ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
LayoutInflater.from(mContext).inflate(resId, contentParent)。

Window

So what is Window, why there is a window, let's understand Window first.

Our mobile phone has many applications, and each application has many pages, so when the mobile phone needs to display which page, at this time, there needs to be a mechanism to manage which page is currently displayed.

Therefore, a system service WindowManagerService (WMS) was created in the Android system to manage the windows on the screen, and the View can only be displayed on the corresponding window. If it does not meet the requirements, the window will not be opened and the corresponding View will not be displayed. The mechanism is to manage the display of views on the screen and the transmission of touch events

Window is an abstract class, and its real implementation is PhoneWindow, which holds a View (root View) inside.
A Window is bound to a View, and ViewRootImpl is used to suggest a relationship between them .
So a Window corresponds to a ViewRootImpl corresponds to a View ,
and the View cannot exist alone, it must be attached to the window, where there is a view, there is a window, and where there is a window, there is a view .

In other words, you can understand that each page view on the mobile phone is actually a Window, and what the page looks like is represented by the View under ViewRootImpl.setView. The click event is also received by the Window and assigned to the View, and then according to Which sub-view of the View is distributed to consume this event in specific circumstances.

Window class level

There are three types of Window, system Window, application Window, and sub-Window.
System Window is a Window that needs to declare permissions on androidMinifest to create, such as Toast and system status bar.
The application class Window corresponds to an Activity.
A child Window cannot exist alone, and needs to be attached to a specific parent Window, such as a common Dialog.

The Window class level is specified by the type parameter of WindowManager.LayoutParams.

Window management WindowManager

The management window relies on WindowManager ,

WindowManager is an interface
that inherits from ViewManager.

ViewManager has 3 important methods.
addView, updateViewLayout, removeView

So WindowManager also has these three methods.
Its real implementation is WindowManagerImpl , and WindowManagerImpl internally hands over the actual work content to WindowManagerGLobal , so the real implementation is actually the WindowManagerGLobal class.

This WindowManagerGLobal class manages all view, ViewRootImpl, WindowManager.LayoutParam and their corresponding relationship

    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList‹ViewRootImpl>mRoots =new ArrayList‹ViewRootImp1>();
    private final ArrayList<WindowManager.LayoutParams›mParams =new ArrayList<WindowManager.LayoutParams>();

mViews retains all views
mRoots retains all ViewRootImpl
mParams retains all layout parameters

3 methods of WindowManager. addView, updateViewLayout, and removeView
are all for adding, updating and deleting views, not for windows, which also verifies what we said earlier

Window is abstract, it has no entity, the real entity is actually the view connected by window, there is a window, it means there is a view, so use
WindowManager to add, delete, update and operate Window, in fact, it can be said that WindowManager adds, deletes and updates under a window View. Deleting a window removes the view under the window is to delete a window.

IPC communication between WindowManager and WMS

The WindowManager instance in each application process manages all Windows under the current process.
WindowManager in the application process adds, deletes, and updates operations on Window, etc., is actually
an AIDL communication between WindowManager and the system process WMS under the application process. The binder object here is that Session
WMS will keep each Session binder object for each process management.

So there are three methods called by WindowManager in the process. addView, updateViewLayout, removeView.
In fact, it calls the corresponding method of the proxy object of the binder object Session. Then the binder Session of WMS receives and calls the corresponding method

Let's look at addView, updateViewLayout, removeView

How to add a Window using WindowManager

Using WindowManager to add a Window is
windowManager.addView to add a view, in fact, it is to add a window and bind the view through viewRootImpl

        WindowManager windowManager = getWindowManager();
        View view;
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        layoutParams.x = 100;
        layoutParams.y = 100;
        layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        windowManager.addView(view,layoutParams);

Set the properties of this window through WindowManager.LayoutParams layoutParams.
Among them, the data layoutParams.type represents the type of the window.
layoutParams.flags indicates that the label attribute of the window
can be selected as follows:

FLAG_NOT_FOCUSABLE

Indicates that Window does not need to acquire focus, nor does it need to receive various input events. This flag will enable FLAG_NOT_TOUCH_MODAL at the same time, and the event will be passed directly to the underlying Window with focus, which is equivalent to not needing events.

FLAG_NOT_TOUCH_MODAL

In this mode, the system will pass the click event outside the current Window area to the underlying Window, and the click event within the current Window area will be handled by itself. This flag is very important. Generally speaking, this flag needs to be turned on, otherwise other Windows will not be able to receive the click event. It is equivalent to needing events yourself, and others needing

FLAG_SHOW_WHEN_LOCKED

Turning on this mode allows Window to be displayed on the lock screen interface.

We said earlier that the real implementation of the WindowManager method is actually WindowManagerGLobal, so the real effect of windowManager.addView depends on
WindowManagerGLobal.addview

In this method, the corresponding viewRootImpl will be created for the current view , and then the view, viewRootImpl, and WindowManager.LayoutParams will be saved in the mViews, mRoots, and mParams lists in the WindowManagerGLobal class mentioned above.

Then call the ViewRootImpl.setView method and talk about it later.

Then through the proxy object of the WMS binder and the WMS process an IPC communication to add the window.

Window deletion

windowManager provides two deletion interfaces,
namely

        windowManager.removeView(view);//同步删除
        windowManager.removeViewImmediate(view);//异步删除

我们说了 windowManager.removeView> WindowManagerGLobal.removeView
windowManager.removeViewImmediate> WindowManagerGLobal.removeViewImmediate

In fact, no matter which one is deleted, the die() method called by the viewRootImpl corresponding to the view .

The difference between asynchronous and synchronous deletion is that if it is a synchronous deletion, it will directly call the die() of viewRootImpl to delete,
and if it is an asynchronous deletion, a message will be sent first. At this time, the view has not been deleted. It will be saved in
the list object of ArraysetmDyingViews in WindowManagerGLobal. This list object is specially used to save these views that have not been deleted.

After the message is sent, it is accepted by the handle in viewRootImpl, and when it is received, call die() of viewRootImpl to delete it.

What will be done in the die() method of this viewRootImpl:

  • Delete the corresponding data of mViews, mRoots, mParams of WindowManagerGLobal
  • The proxy object of the binder of WMS communicates with the WMS process through an IPC to delete the window
  • Call the View.dispatchDetachedFromWindows() method, which will call the
    onDetachedFromWindow() method of the View internally, that is, when the view is removed from the window, the
    **onDetachedFromWindow()** method of the View will be called instead, and we can rewrite this method , do some resource recycling in this method, such as terminating the animation, stopping the current thread, etc.

Windows update

 windowManager.updateViewLayout(view,WindowManager.LayoutParams);

The update of the window is mainly based on the WindowManager.LayoutParams layout of the view. For example, if some information such as position has changed, you can call

Of course, windowManager.updateViewLayout is actually WindowManagerGLobal.updateViewLayout.
In this method, it will reset a new layout parameter for view.setLayoutParams , and then call viewRootImpl of view to call
viewRootImpl.setLayoutParams()

Then call scheduleTraversals in this method to redraw the view

Of course, viewRootImpl will still update the window through an IPC communication with the WMS process through the proxy object of the WMS binder

How is View drawn

We said earlier that the real implementation of the WindowManager method is actually WindowManagerGLobal, so the real effect of windowManager.addView depends on
WindowManagerGLobal.addview

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ……

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            final Context context = view.getContext();
            if (context != null
                    && (context.getApplicationInfo().flags
                            & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
            }
        }

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
             ……
            // 创建ViewRootImpl
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            try {
                //调用ViewRootImpl.setView
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

So, windowManager.addView adds a window view, in fact, the ViewRootImpl corresponding to the window calls setView to bind a view to the window. Let's look at setview

#ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
      
            requestLayout();
            
            try {
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                        mTempInsets);
                setFrame(mTmpFrame);
            } catch (RemoteException e) {
            }
            //省略
            //输入事件接收
        }
    }
}

The key to the setview method is to call the requestLayout() method. Let’s continue to look at the requestLayout method.

ViewRootImpl.java
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        //绘制请求
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //主要是将mTraversalRunnable 放入队列
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        //省略
    }
}

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

void doTraversal() {
    //没有取消绘制的话则开始绘制
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        //真正开始执行measure、layout、draw等方法
        performTraversals();
    }
}

That is, requestLayout calls scheduleTraversals, and in the scheduleTraversals method, a Runnable is sent through the main thread handle.
Call performTraversals in Runnable. Really start to implement methods such as measure, layout, and draw.

Then the whole process is as follows
insert image description here

Next, let's learn how to draw View

Coordinate System

There are two coordinate systems in the Android system, namely the Android coordinate system and the View coordinate system.

Android coordinate system
In Android, the vertex in the upper left corner of the screen is used as the origin of the Android coordinate system. The right of this origin is the positive direction of the X axis, and the downward direction is the positive direction of the Y axis.
insert image description here

View coordinate system
It does not conflict with the Android coordinate system, the two exist together, and together they help developers better control the View.
insert image description here
From the above figure, we can know the very important coordinates:

Let’s start with the width and height of the view itself:
width=getRight-getleft;
Height=getbottom-gettop;
of course, this is obviously a bit troublesome, because the system has provided us with methods to obtain the width and height of the View. getHeight() is used to get the height of the View itself, and getWidth() is used to get the width of the View itself.

The coordinates of the View itself (note that the phasor with the parent layout is not the entire screen).
The distance from the View to its parent control (ViewGroup) can be obtained by the following method.
• getTop(): Get the distance from the top edge of View itself to the top edge of its parent layout.
• getLeft(): Get the distance from the left side of View itself to the left side of its parent layout.
• getRight(): Get the distance from the right side of the View itself to the left side of its parent layout.
• getBottom(): Get the distance from the bottom edge of View itself to the top edge of its parent layout.

are measured from the upper left

The method provided by .MotionEvent
The dot in the middle of the figure is assumed to be the point we touch. We know that whether it is View or ViewGroup, the final click event will be handled by the onTouchEvent (MotionEvent event) method. MotionEvent plays an important role in user interaction, and it provides many event constants internally, such as our commonly used ACTION_DOWN, ACTION_UP and ACTION_MOVE. In addition, MotionEvent also provides various methods for obtaining focus coordinates.
• getX(): the distance from the point clicked on the view to the left side of the view
• getY(): the distance from the point clicked on the view to the top of the view
• getRawX(): the distance from the clicked point to the left side of the entire screen
• getRawY(): click The distance from the point to the top of the entire screen

The drawing process of view

The drawing of View is
view.measure() to measure the measured width and height of this view
. view.layout() to determine the final width, height and placement position of this view.
**view.draw()** draws this view

Note that the measured width and height of view.measure() can only be obtained through getMeasuredWidth();
only after view.measure() is completed, and view.layout() can be obtained through getWidth() only after view.layout() is laid out The final width and height,
generally the width and height of view.measure() is the final width and height.

measure

measure process

Regardless of the measure process of viewgroup and view, they are actually the same. Viewgroup is also a view. When you want to measure a view, you call view.measure(int widthMeasureSpec, int heightMeasureSpec). The parameter MeasureSpec is the view MeasureSpec.
insert image description here
Entry: measure() source code

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
            mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {

        // 调用onMeasure()计算视图大小
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    } else {
        ...
}

measure源码只需要知道它的作用只是调用onmeasure,并传入view的MeasureSpec进入onmeasure里面就没了。

The source code of onmeasure is as follows:

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 参数说明:View的MeasureSpec

    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  

}

Analysis: There are 3 methods in onMeasu.

setMeasuredDimension: Set the measured width and height of the view
getDefaultSize(): Get the width and height of the view
getSuggestedMinimumWidth(): Give the view a default value.

We can see from the code that the parameter set by setMeasuredDimension() is the width and height of the view, and the width and height values ​​are obtained from getDefaultSize(). And
getSuggestedMinimumWidth() is to give the view a default value. Here we need to distinguish that the measureSpec parameter already has the size of the view. However, here we will provide a size for the view, and this size is obtained through getSuggestedMinimumWidth().

Let's look at the source code of getDefaultSize()

  public static int getDefaultSize(int size, int measureSpec) {  

        // 参数说明:
        // size:提供给view的一个大小  ,
        // measureSpec:view的measureSpec

            // 设置默认大小
            int result = size; 
            
            // 获取宽/高测量规格的模式 & 测量大小
            int specMode = MeasureSpec.getMode(measureSpec);  
            int specSize = MeasureSpec.getSize(measureSpec);  
          
            switch (specMode) {  
                // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
                case MeasureSpec.UNSPECIFIED:  
                    result = size;  
                    break;  

                // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
                case MeasureSpec.AT_MOST:  
                case MeasureSpec.EXACTLY:  
                    result = specSize;  
                    break;  
            }  

         // 返回View的宽/高值
            return result;  
        }

From the getDefaultSize code, we can know that the default value will only work if the MeasureSpec of the view is UNSPECIFIED, otherwise, the value obtained by getDefaultSize, that is, the value of setMeasuredDimension() is the size value in the MeasureSpec of the view.
And MeasureSpec, the mode UNSPECIFIED will not be used, so, from the overall source code of view.measure(), the measured value of view.measure() is the size value of MeasureSpec of view. , regardless of whether the view sets match, dp or wrap in the xml, the effect is the size value of the view's MeasureSpec.

All summed up as

When calling view.measure(int widthMeasureSpec, int heightMeasureSpec), you must already know the MeasureSpec of this view, and then call view.measure(int widthMeasureSpec, int heightMeasureSpec), measure
will call onmeasure(), and in onmeasure pass setMeasuredDimension( width, height) set the width and height of the view. And this value comes from the logic in the getDefaultSize(int size, int measureSpec) function, and the value given by getDefaultSize is the size value of the MeasureSpec of the view, that is, the measurement value of the entire source code process of view.measure() is the MeasureSpec of the view The size value. , regardless of whether the view sets match, dp or wrap in the xml, the effect is the size value of the view's MeasureSpec.
So from here we can be inspired that the MeasureSpec of this view determines the width and height of the view, which is very important, and we can also get an inspiration. If the size of the MeasureSpec of this view is not the value we want, we can completely rewrite onmeasure. Get the correct value according to the logic and then go to setMeasuredDimension().

So what is MeasureSpec? How to get the MeasureSpec of the view

MeasureSpec = measurement mode (mode) + measurement size (size)

There are 3 types of measurement modes: UNSPECIFIED, EXACTLY and AT_MOST

EXACTLY ; view sets match, or dp.

AT_MOST : view sets wrap.

UNSPECIFIED: The view does not constrain the sub-view, which is generally not used. ignore

size: the size of the view.

The MeasureSpec value of View is calculated based on the layout parameters (LayoutParams) of View and the MeasureSpec value of the parent container

That is, the MeasureSpec of the view needs to be made by the parent view, that is, the MeasureSpec of the child view is made by the parent class. The parent
view will call the system code getChildMeasureSpec(int spec, int padding, int childDimension) to return the MeasureSpec of this view. Therefore, the measurement width and height of the view depends on the size of the MeasureSpec of the view, and the MeasureSpec comes from the parent class calling
getChildMeasureSpec( ), so the getChildMeasureSpec code is very important.
The source code is as follows:

 public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

         //参数说明
         * @param spec 父view的MeasureSpec 
         * @param padding 父view的内边距 
         * @param childDimension 子view设置的size

            //父view的测量模式
            int specMode = MeasureSpec.getMode(spec);     

            //父view的大小
            int specSize = MeasureSpec.getSize(spec);     
          
            //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
            int size = Math.max(0, specSize - padding);  
          
            //子view想要的实际大小和模式(需要计算)  
            int resultSize = 0;  
            int resultMode = 0;  
          
            //通过父view的MeasureSpec和子view的LayoutParams确定子view的 MeasureSpec

            switch (specMode) {  
            // 当父view的模式为EXACITY时
            case MeasureSpec.EXACTLY:  
                // 当子view的LayoutParams>0,即子view设为多少dp
                if (childDimension >= 0) {  
                    //子view大小为子自身所赋的值,模式大小为EXACTLY  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 当子view的LayoutParams为MATCH_PARENT时(-1)  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    //子view大小为父view要求的大小,模式为EXACTLY  
                    resultSize = size;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    //子view决定自己的大小,但最大不能超过父view,但是这里还是设置了子view的大小为父view要求的大小模式为AT_MOST  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 当父view的模式为AT_MOST时  
            case MeasureSpec.AT_MOST:  
                // 道理同上  
                if (childDimension >= 0) {  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
            // 多见于ListView、GridView  
            case MeasureSpec.UNSPECIFIED:  
                if (childDimension >= 0) {  
                    // 子view大小为子自身所赋的值  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                }  
                break;  
            }  
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
        }  

The above code of getChildMeasureSpec draws a conclusion.
The value set at the end of the view.measure() process is to look at view.MeasureSpec.size, this value, and this MeasureSpec comes from getChildMeasureSpec called by the parent class, and From the point of view of getChildMeasureSpec,
if the view sets how much dp in xml, then the final value measured by view.measure() is how many dp,
if the view sets match in xml, then the final value measured by view.measure() is what the parent class can give it The maximum value, that is
(the length of the parent class - the inner edge of the parent class setting)
If the view sets wrap in xml, then the final value measured by view.measure() is the maximum value that the parent class can give it, that is
(parent length of the class - inner edge set by the parent class)

In the third view above, when your view sets wrap in xml, the effect does not include itself, but it is the same as the view setting match in xml. This is very unreasonable , so when your custom view is needed in xml Set the wrap to include the requirement of its own width, and at this time, it is necessary to rewrite
onmeasure()
, and set setMeasuredDimension(width, height) according to its own situation.

code show as below:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //最终测量长宽
        int endHeight=0, endWidth=0;

        //view的MeasureSpec
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //如果view宽高都设置了dp或者match那么不需要计算逻辑直接最终宽高就是view设置的dp或者match
        if(widthMode==MeasureSpec.EXACTLY&&heightMode==MeasureSpec.EXACTLY){
            endHeight=heightSize;
            endWidth=widthSize;
            setMeasuredDimension(endWidth, endHeight);
            return;
        }
        //如果view宽设置了dp或者match那么宽不需要计算逻辑view的宽就是view设置的dp或者match
        if (widthMode == MeasureSpec.EXACTLY){
            endWidth=widthSize;
        }
        //如果view高设置了dp或者match那么高不需要计算逻辑view的高就是view设置的dp或者match
        if (heightSize == MeasureSpec.EXACTLY){
            endHeight=heightSize;
        }
        //以下根据需要求计算宽或者高或者宽和高的最终值
        //....
        
        //最终设置进去最终值
        setMeasuredDimension(endWidth, endHeight);
    }

Only the //... part of the above needs to be changed, and the rest of the code remains unchanged.

From the above, we know that the viewgroup and view of the view are both a view, and the measure process is the same. However, for the viewgroup, when it calculates its own width and height, it often needs to check the size of the subview according to the situation. Set the width and height, similar to Linearlayout, whether the sub-views are arranged horizontally or vertically, depending on the size of different viewgroups. Of course, viewgroup can also directly write dp or match, or wrap regardless of the subview (from the above we know that writing wrap has the same effect as writing match). That is to say, viewgroup needs to traverse the size of sub-views in the above //... part, and then set the size of viewgroup according to requirements, and everything else is the same as a single view.

The following is the process of rewriting the onMeasure logic of the viewgroup

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //最终测量长宽
        int endHeight = 0, endWidth = 0;

        //view的MeasureSpec
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //如果view宽高都设置了dp或者match那么不需要计算逻辑直接最终宽高就是view设置的dp或者match
        if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
            endHeight = heightSize;
            endWidth = widthSize;
            setMeasuredDimension(endWidth, endHeight);
            return;
        }
        //如果view宽设置了dp或者match那么宽不需要计算逻辑view的宽就是view设置的dp或者match
        if (widthMode == MeasureSpec.EXACTLY) {
            endWidth = widthSize;
        }
        //如果view高设置了dp或者match那么高不需要计算逻辑view的高就是view设置的dp或者match
        if (heightSize == MeasureSpec.EXACTLY) {
            endHeight = heightSize;
        }

        //以下根据需要求计算宽或者高或者宽和高的最终值
        //测量子view确定最终的宽高
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //最终值是子view的宽全加起来,高全加起来,当然这里只是个模板样式,具体根据需求怎么来
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            endHeight += view.getHeight();
            endWidth += view.getWidth();
        }
        //设置最终宽高
        setMeasuredDimension(endWidth, endHeight);
    }

It can be seen from the above:
only the final value of width or height or width and height needs to be calculated according to the needs of // the following needs to realize the final value according to the sub-view, and the others are the same as a single view, so we
just Just focus on how to measure the child view to get the final value according to the requirement.

In the above measureChildren(widthMeasureSpec, heightMeasureSpec); is the system method called.
The parameter is the MeasureSpec of the viewgroup

The following is the source code of measureChildren(widthMeasureSpec, heightMeasureSpec)

/**

Traverse child View & call measureChild()
**/

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    // 参数说明:父视图的测量规格(MeasureSpec)

            final int size = mChildrenCount;
            final View[] children = mChildren;

            // 遍历所有子view
            for (int i = 0; i < size; ++i) {
                final View child = children[i];
                 // 调用measureChild()进行下一步的测量 ->>分析1
                if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                    measureChild(child, widthMeasureSpec, heightMeasureSpec);
                }
            }
        }

/**
Call getChildMeasureSpec through the parent view's MeasureSpec and each child view layout parameter LayoutParams to get the MeasureSpec of each child View, and then call the measure() of the child View to get the measured width and height of the child view**
/

  protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {

        //  获取子viewLayoutParams
        final LayoutParams lp = child.getLayoutParams();
        // 根据父视图的MeasureSpec & 子viewLayoutParams,计算单个子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);//参数为viewgroup的MeasureSpec,viewgroup的内边距,子view的宽高。
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
        //  调用子View的measure()去得到子view的测量宽高
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    // 回到调用原处

We verified it by looking at the whole source code of measureChildren(). The initial conclusion is that the MeasureSpec value of the view is based on the view layout parameter LayoutParams and the MeasureSpec of the parent view, and the MeasureSpec of the view is called by the parent class to
getChildMeasureSpec () Get the MeasureSpec of the view, call
view.measure(childWidthMeasureSpec, childHeightMeasureSpec); to measure a view, you need to get the MeasureSpec of the view first, and call getChildMeasureSpec() to get this value from the parent class.

Note that in the viewgroup above, the purpose of calling measureChildren() to measure each view is to arrange the size of the view group according to the size of the subview, but the size of each subview measured does not include the subview set in xml The outer margin, this outer margin is also the range that the viewgroup needs to calculate. That is, we need to get the outer margin of each subview.
Can we use the LayoutParams of the subview to get layout_marginTop? In fact, it is not possible, because LayoutParams does not contain the margin attribute of the view. MarginLayoutParams actually contains the margin attribute of the view. Class, MarginLayoutParams is a subclass of LayoutParams, so if we want to get the margin attribute, we have to use MarginLayoutParams.

Rewrite the function and return MarginLayoutParams


 @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

That is, the rewritten onmeasure template of the custom viewgroup is as follows

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //最终测量长宽
        int endHeight = 0, endWidth = 0;

        //view的MeasureSpec
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //如果view宽高都设置了dp或者match那么不需要计算逻辑直接最终宽高就是view设置的dp或者match
        if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
            endHeight = heightSize;
            endWidth = widthSize;
            setMeasuredDimension(endWidth, endHeight);
            return;
        }
        //如果view宽设置了dp或者match那么宽不需要计算逻辑view的宽就是view设置的dp或者match
        if (widthMode == MeasureSpec.EXACTLY) {
            endWidth = widthSize;
        }
        //如果view高设置了dp或者match那么高不需要计算逻辑view的高就是view设置的dp或者match
        if (heightSize == MeasureSpec.EXACTLY) {
            endHeight = heightSize;
        }

        //以下根据需要求计算宽或者高或者宽和高的最终值
        //测量子view确定最终的宽高
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //最终值是子view的宽高并且外边距加起来,当然这里只是个模板样式,具体根据需求怎么来
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            endHeight += view.getHeight()+((MarginLayoutParams)view.getLayoutParams()).topMargin+((MarginLayoutParams)view.getLayoutParams()).bottomMargin;
            endWidth += view.getWidth()+((MarginLayoutParams)view.getLayoutParams()).leftMargin+((MarginLayoutParams)view.getLayoutParams()).rightMargin;
        }
   
        //设置最终宽高
        setMeasuredDimension(endWidth, endHeight);
    }
    
@Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

Through the summary of the measure process of view, inheriting a view or inheriting a viewgroup does not need to rewrite onmeasure. For a single view, it is because the wrap effect is set in the xml to be the same as the match effect, so it needs to be rewritten according to the requirements. Calculate the final height, and for the viewgroup,
because the wrap effect is set in the xml, it is rewritten to calculate the final height according to the requirements, and when measuring the height, it is necessary to measure each The size of the child view, and then set its own size according to the size of the child view as needed

layout process

insert image description here

For the layout process of the view, you only need to know that when a view is placed, whether it is a viewgroup or a single view, it is called view.layout
(Left, Top, Right, Bottom); the 4 parameters are the view relative The position of the parent container is not relative to the position of the mobile phone. When calling
view.layout(Left, Top, Right, Bottom); after confirming the placement of this view, it will automatically call onlayout internally. The sub-views of the view are arranged. If it is a single view, there is nothing wrong. The single view is empty. If it is a viewgroup, there will be corresponding placement logic.
onLayout(boolean changed, int l, int t, int r , int b) The parameters all indicate the hunger of the parent view, that is, after view.layout(Left, Top, Right, Bottom) determines the position of the view, then the position of the view and the position of the view relative to the previous placement The status of whether to change is passed to the child view through the onLayout parameter.
So there is no need to re-onlayout for a single view, only the direct inheritance of viewgroup requires re-onlayout

draw

insert image description here

For the draw process of the view, you only need to know that when drawing a view, whether it is a viewgroup or a single view, view.draw() is called . After calling, the specific drawing ondraw() function will be called step by step internally. After drawing the view, if the view has subviews, it will call dispathDraw() and call the draw and then ondraw() of the subviews one by one.

So far, the drawing process of view has been explained.

custom view

There are several ways to create a custom view

Inheriting system View controls such as TextView and other system controls, expand the basic functions of system controls
Inheriting View does not reuse system control logic, inheriting View for function definition
Inheriting system ViewGroup For example inheriting from LinearLayout and other system controls, in the system controls Extend basic functions
Inherit ViewViewGroup Do not reuse system control logic, inherit ViewGroup for function definition

Before explaining the custom view, first define the custom view attribute.

custom view attribute

The controls of the Android system starting with android are the attributes that come with the system view. And we can directly set some view attributes for our custom view to use. Note that custom attributes can only be used for custom views, not for system views.

a. How to customize attributes

Custom attributes in attrs.xml in res/values. For example

<declare-styleable name="TestView">    
   <attr name="attrone" format="dimension"/>    
   <attr name="attrtwo" format="string" >    
    <enum name="one" value="0"/>    
    <enum name="two" value="1"/>
   </attr>
</declare-styleable>

Analyze the meaning of the above code:
declare-styleable: Indicates an attribute group. Its name must be the same as the name of your custom view.
attr: Indicates a single attribute. format represents the format of the attribute. Format includes many kinds: such as color, value, enumeration and so on. Look at the picture below:

insert image description here
Here is a summary of the attribute definition format:


 1   <declare-styleable name="TextView">
         2  <attr  name ="属性1" format = "这个属性的取值格式">
            3  <enum name="取值1" value="程序中对应的值"/>
               <enum name="取值1" value="程序中对应的值"/>
               <enum name="取值1" value="程序中对应的值"/>
               <enum name="取值1" value="程序中对应的值"/>
            4  <flag name="取值1" value="程序中对应的值" />
               <flag name="取值2" value="程序中对应的值" />
               <flag name="取值3" value="程序中对应的值" />
    </declare-styleable>

Among them, 3 and 4 can be omitted, and 3 means that we have set several values ​​for this attribute in advance, and can be directly selected from these values. The difference with 4 is: the flag can be used in the layout file as value 1|value 2, that is to say, it can take multiple values.

b. How to use custom attributes?

First, add the namespace in the xml file using the custom view: xmlns:app="http://schemas.android.com/apk/res-auto"
before you can use the app on the custom view:, And android: is the property of the system. like:

<com.mg.axe.androiddevelop.view.TestView   
   android:layout_width="match_parent"    	
   android:layout_height="match_parent"    
   app:attrone="10dp"   
   app:attrtwo="two"    />

c. How to obtain custom attributes?
In the constructor of the custom view, use TypedArray to get the corresponding properties

typedArray.getInt(int index, float defValue);
typedArray.getDimension(int index, float defValue);
typedArray.getBoolean(int index, float defValue);
typedArray.getColor(int index, float defValue);
typedArray.getString(int index)
typedArray.getDrawable(int index);
typedArray.getResources();

The parameter index is the attribute set in your custom attribute group.
The format is R.styleable...
The parameter defValue is the default value

When customizing the view, there will be 4 constructors, for example as follows

 public CarsonView(Context context) {
        super(context);
        // 如果View是在Java代码里面new的,则调用第一个构造函数
    }


    public  CarsonView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 如果View是在xml里写入,则调用第二个构造函数
// 自定义属性在这个函数内获得
    }


    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
    }

 
    public  CarsonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
           //API21之后才使用
    // 不会自动调用
    // 一般是在第二个构造函数里主动调用
    // 如View有style属性时
    }

Among them, the first and second ones are commonly used, but the third and fourth are not commonly used. The parameters int defStyleAttr and int defStyleRes, if you don’t understand, go to Baidu.

Note that after using TypedArray, remember to typedArray.recycle();

One of the usage scenarios of TypedArray is the above-mentioned custom View, which will be created every time the Activity is created. Therefore, the system needs to create the array frequently, which is a big overhead for memory and performance. If the pool mode is not used , Let the GC recycle every time, it is likely to cause OutOfMemory.

The specific use is as follows:

<declare-styleable name="TestView">
        <attr name="attrone" format="dimension"/>
        <attr name="attrtwo" format="string" >
            <enum name="one" value="0"/>
            <enum name="two" value="1"/>
        </attr>
    </declare-styleable>


    <declare-styleable name="RadialGradientButton">
        <attr name="minRadius" format="integer"/>
        <attr name="centerColor" format="reference" />
        <attr name="edgeColor" format="reference" />
    </declare-styleable>
 <com.example.administrator.module.CustomView.RadialGradientButton
       android:layout_width="50dp"
       android:layout_height="50dp"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       app:minRadius="20"
       app:centerColor="@color/colorPrimaryDark"
       app:edgeColor="@color/colorAccent"></com.example.administrator.module.CustomView.RadialGradientButton>


 private void init( Context context,AttributeSet attrs) {
    
    
        setLayerType(LAYER_TYPE_SOFTWARE, null);
 
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RadialGradientButton);
        minRadius=typedArray.getInt(R.styleable.RadialGradientButton_minRadius,10);
        centerColor= typedArray.getResourceId(R.styleable.RadialGradientButton_centerColor,0x00FFFFFF);
        edgeColor = typedArray.getResourceId(R.styleable.RadialGradientButton_edgeColor, 0xFF58FAAC);
        typedArray.recycle();
     
    }

    public RadialGradientButton(Context context, AttributeSet attrs) {
    
    
        super(context, attrs);
        init(context,attrs);
    }

ok, continue to talk about custom view

Inherit the View subclass , take TextVIew as an example.

Requirements: Set the background for TextVIew and add a horizontal line in the middle of the layout.

public class LineTextView extends TextView {

    //定义画笔,用来绘制中心曲线
    private Paint mPaint;
    
    /**
     * 创建构造方法
     * @param context
     */
    public LineTextView(Context context) {
        super(context);
        init();
    }

    public LineTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LineTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
    }

    //重写draw方法,绘制我们需要的中间线以及背景
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        mPaint.setColor(Color.BLUE);
        //绘制方形背景
        RectF rectF = new RectF(0,0,width,height);
        canvas.drawRect(rectF,mPaint);
        mPaint.setColor(Color.BLACK);
        //绘制中心曲线,起点坐标(0,height/2),终点坐标(width,height/2)
        canvas.drawLine(0,height/2,width,height/2,mPaint);
    }
}

Then use it in xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">
<com.example.administrator.module.CustomView.LineTextView
    android:layout_width="50dp"
    android:layout_height="30dp"
    android:text="sssdf"/>

</LinearLayout>

Inheriting the View subclass does not need to rewrite the measure and layout processes, because we want to reuse the measure and layout of the corresponding system controls. We only need to rewrite the draw process, specifically the onDraw function, and draw the desired effect through strokes.

b. Directly inherit View

public class RectView extends View {
    //定义画笔
    private Paint mPaint = new Paint();

    /**
     * 实现构造方法
     *
     * @param context
     */
    public RectView(Context context) {
        super(context);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint.setColor(Color.BLUE);

    }

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //最终测量长宽
        int endHeight=0, endWidth=0;

        //view的MeasureSpec
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //如果view宽高都设置了dp或者match那么不需要计算逻辑直接最终宽高就是view设置的dp或者match
        if(widthMode==MeasureSpec.EXACTLY&&heightMode==MeasureSpec.EXACTLY){
            endHeight=heightSize;
            endWidth=widthSize;
            setMeasuredDimension(endWidth, endHeight);
            return;
        }
        //如果view宽设置了dp或者match那么宽不需要计算逻辑view的宽就是view设置的dp或者match
        if (widthMode == MeasureSpec.EXACTLY){
            endWidth=widthSize;
        }
        //如果view高设置了dp或者match那么高不需要计算逻辑view的高就是view设置的dp或者match
        if (heightSize == MeasureSpec.EXACTLY){
            endHeight=heightSize;
        }
        //以下根据需要求计算宽或者高或者宽和高的最终值
        //....
        
        //最终设置进去最终值
        setMeasuredDimension(endWidth, endHeight);
    }

    /**
     * 重写draw方法
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取各个编剧的padding值
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //获取绘制的View的宽度
        int width = getWidth() - paddingLeft - paddingRight;
        //获取绘制的View的高度
        int height = getHeight() - paddingTop - paddingBottom;
        //绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
        canvas.drawRect(0 + paddingLeft, 0 + paddingTop, width + paddingLeft, height + paddingTop, mPaint);
    }
}

Direct inheritance of view needs to rewrite ondraw, depending on the situation, rewrite onmeasure()

Q: Why does onmeasure need to be rewritten if the view is directly inherited, but the view of the inherited system does not need it?
Because the source code of the directly inherited view system has been written and when the view is set to wrap in xml, no match will occur.

c Inheritance system viewgroup

Pay attention when inheriting the system viewgroup. By default, the ondraw method will not be called. If you want to enable the ondraw method, you need to call setWillNotDraw(false);
that is to combine multiple controls into a new control, mainly to solve the problem of reusing the same type of layout multiple times. Like our HeaderView and Dailog at the top, we can combine them into a new control.

We use a custom HeaderView instance to understand the usage of custom composite controls.

  1. Write the layout file
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:id="@+id/header_root_layout"
    android:layout_height="45dp"
    android:background="#827192">

    <ImageView
        android:id="@+id/header_left_img"
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:layout_alignParentLeft="true"
        android:paddingLeft="12dp"
        android:paddingRight="12dp"
        android:src="@drawable/back"
        android:scaleType="fitCenter"/>

    <TextView
        android:id="@+id/header_center_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:lines="1"
        android:maxLines="11"
        android:ellipsize="end"
        android:text="title"
        android:textStyle="bold"
        android:textColor="#ffffff"/>
    
    <ImageView
        android:id="@+id/header_right_img"
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:layout_alignParentRight="true"
        android:src="@drawable/add"
        android:scaleType="fitCenter"
        android:paddingRight="12dp"
        android:paddingLeft="12dp"/>

</RelativeLayout>

2. Write in java code

public class YFHeaderView extends RelativeLayout {

    private ImageView img_left;
    private TextView text_center;
    private ImageView img_right;
    private RelativeLayout layout_root;
    private Context context;
    String element;

    private int showView;

    public YFHeaderView(Context context) {
        super(context);
        this.context = context;
        initView(context);
    }

    public YFHeaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        initView(context);
        initAttrs(context, attrs);
    }

    public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        initView(context);
        initAttrs(context, attrs);
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);
        String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
        if (!TextUtils.isEmpty(title)) {
            text_center.setText(title);
        }
        showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views, 0x26);
        text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));
        mTypedArray.recycle();
        showView(showView);

    }

    private void showView(int showView) {
        Long data = Long.valueOf(Integer.toBinaryString(showView));
        element = String.format("%06d", data);
        for (int i = 0; i < element.length(); i++) {
            if(i == 0) ;
            if(i == 1) text_center.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 2) img_right.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 3) ;
            if(i == 4) img_left.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 5) ;
        }

    }

    private void initView(final Context context) {
        LayoutInflater.from(context).inflate(R.layout.view_header, this, true);
        img_left = (ImageView) findViewById(R.id.header_left_img);
        img_right = (ImageView) findViewById(R.id.header_right_img);
        text_center = (TextView) findViewById(R.id.header_center_text);
        layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);
        layout_root.setBackgroundColor(Color.BLACK);
        text_center.setTextColor(Color.WHITE);

        img_left.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(context, element + "", Toast.LENGTH_SHORT).show();
            }
        });
    }

    private void setTitle(String title) {
        if (!TextUtils.isEmpty(title)) {
            text_center.setText(title);
        }
    }


    private void setLeftListener(OnClickListener onClickListener) {
        img_left.setOnClickListener(onClickListener);
    }

    private void setRightListener(OnClickListener onClickListener) {
        img_right.setOnClickListener(onClickListener);
    }

}

3. Set in the required layout file

    <com.example.yf.view.YFHeaderView
        android:layout_width="match_parent"
        android:layout_height="45dp"
        app:title_text="标题"
        app:show_views="center_text|left_img|right_img">

    </com.example.yf.view.YFHeaderView>

Inheriting the system viewgroup does not need to rewrite any functions, it just combines multiple controls into one control, and the process used is also the process of the system viewgroup, so there is no need to rewrite.

d. Inherit ViewGroup

Inheriting ViewGroup must rewrite onlayout to place the position of the child view, and rewrite onmeasrue() to set its own size depending on the situation

An example of a custom fluid layout:

package com.example.test;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

public class FlowLayout extends ViewGroup {
    //记录每一行里面那一行中最高的高度,FlowLayout就是以每一行中最高的item为高的
    private List<Integer> arrayLineHeight = new ArrayList<>();


    public FlowLayout(Context context) {
        super(context);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        arrayLineHeight.clear();
        //最终测量长宽
        int endHeight = 0;
        int endWidth = 0;

        //view的MeasureSpec
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //如果view宽高都设置了dp或者match那么不需要计算逻辑直接最终宽高就是view设置的dp或者match
        if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
            endHeight = heightSize;
            endWidth = widthSize;
            setMeasuredDimension(endWidth, endHeight);
            return;
        }
        //如果view宽设置了dp或者match那么宽不需要计算逻辑view的宽就是view设置的dp或者match
        if (widthMode == MeasureSpec.EXACTLY) {
            endWidth = widthSize;
        }
        //如果view高设置了dp或者match那么高不需要计算逻辑view的高就是view设置的dp或者match
        if (heightSize == MeasureSpec.EXACTLY) {
            endHeight = heightSize;
        }
        //如果宽或者高设置的wrap,那么宽或者高就肯定要根据情况去计算
        if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST) {
            //以下根据需要求计算宽或者高或者宽和高的最终值
            //测量子view确定最终的宽高
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            //如果flowLayout一个空间也没,既然设置了wrap,那么这个flowLayout高就为0.
            if (getChildCount() == 0) {
                endHeight = 0;
            } else {
                //onMeasure的目的是测量view的最终宽高,而测量view的子view区域只是view的其中一部分,所以一开始view的最终宽高就得加上view的内边距
                endHeight = endHeight + getPaddingTop() + getPaddingBottom();

                int lineHeight = 0;
                int lineWidth = 0;

                for (int i = 0; i < getChildCount(); i++) {
                    //不断累加每一项目的长度以及它再xml设置的左右边距
                    lineWidth = lineWidth + getChildAt(i).getMeasuredWidth() + ((MarginLayoutParams) getChildAt(i).getLayoutParams()).leftMargin + ((MarginLayoutParams) getChildAt(i).getLayoutParams()).rightMargin;

                    //如果加起来的长度大于最大长,那么证明需要换行,那么数据一切清空,记录上一行的最大高度,重新来过
                    if (lineWidth > endWidth - getPaddingRight() - getPaddingLeft()) {
                        lineWidth = 0;//清空并且再次计算新的一行的长度。
                        endHeight = endHeight + lineHeight;//总长添加上一行的最大值
                        arrayLineHeight.add(lineHeight);//记录上一行的最高高度
                        lineHeight = 0;//lineHeight清空
                        i = i - 1;//回退上一格,因为暴露出来的一项需要拿来添加到新的一行的宽度,如果不回退,那么露出来的一项会被忽略这不合理。
                    }else {
                        //lineHeight表示当前行最大的那个item的高度,当然需要包含上下边距
                        lineHeight = Math.max(lineHeight, getChildAt(i).getMeasuredHeight() + ((MarginLayoutParams) getChildAt(i).getLayoutParams()).topMargin + ((MarginLayoutParams) getChildAt(i).getLayoutParams()).bottomMargin);
                    }
                }
                //遍历完所有的item后,也就是把所有的上一行都加起来了,最后就需要把最后一行的最大item加起来了
                arrayLineHeight.add(lineHeight);//记录最后一行的最高高度
                endHeight = endHeight + lineHeight;//总长添加最后一行的最大值
            }
        }
        setMeasuredDimension(endWidth, endHeight);
        Log.d("zjs", "onMeasure:endWidth " + endWidth + "onMeasure:endHeight " + endHeight);
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int parentPaddingLeft = getPaddingLeft();
        int parentPaddingTop = getPaddingTop();
        int parentPaddingRight = getPaddingRight();
        int parentPaddingBottom = getPaddingBottom();

        //每个孩子固定的变量,而以oldTop,oldLeft这两个跟随移动的值来给予每个孩子正确的值
        int childLeft = 0;
        int childTop = 0;
        int childRight = 0;
        int childBottom = 0;

        //这个变量,用来记录每个item的摆放前它距离它父view上边距的距离,而一开始这个距离就应该等于父view设置的上边距
        int oldTop = parentPaddingTop;
        //这个变量,用来记录每个item的摆放前它距离它父view左边距的距离,而一开始这个距离就应该等于父view设置的左边距
        int oldLeft = parentPaddingLeft;

        int j = 0;

        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);

            //每个item在摆放前的左边距等与他们以左到父view的长度+他们每个人自己设置的左边距
            childLeft = oldLeft + ((MarginLayoutParams) view.getLayoutParams()).leftMargin;
            //每个item在摆放前的上边距等与他们以上行列的高度+他们每个人自己设置的上边距
            childTop = oldTop + ((MarginLayoutParams) view.getLayoutParams()).topMargin;

            childRight = childLeft + view.getMeasuredWidth();
            childBottom = childTop + view.getMeasuredHeight();

            //如果到达了换行的那一格时候,oldLeft要回到初始值,oldTop要是以上的高度,然后孩子的四个位置重新赋值。
            if (childRight >= getMeasuredWidth() - parentPaddingLeft - parentPaddingRight) {
                oldLeft = parentPaddingLeft;
                oldTop += arrayLineHeight.get(j);

                //每个item在摆放前的左边距等与他们以左到父view的长度+他们每个人自己设置的左边距
                childLeft = oldLeft + ((MarginLayoutParams) view.getLayoutParams()).leftMargin;
                //每个item在摆放前的上边距等与他们以上行列的高度+他们每个人自己设置的上边距
                childTop = oldTop + ((MarginLayoutParams) view.getLayoutParams()).topMargin;

                childRight = childLeft + view.getMeasuredWidth();
                childBottom = childTop + view.getMeasuredHeight();

            }
            view.layout(childLeft, childTop, childRight, childBottom);
            //oldLeft要是当前view的childRight+当前view设置的右边距。这样下一个view摆放的位置前的oldLeft才是正确的值
            oldLeft = childRight + ((MarginLayoutParams) view.getLayoutParams()).rightMargin;

        }
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
}

Replenish

Custom View drawing process function call chain (simplified version)
insert image description here
There is an onSizeChanged method here. This method is very convenient, this function is called when the size of the view changes. Its purpose is to determine the final width and height of the view,

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    
    
    super.onSizeChanged(w, h, oldw, oldh);
}

It can be seen that it has four parameters, namely width, height, last width, and last height.

And also note that invalidate() only re-drops ondraw

Special attention:
api25-24: Execute 2 onMeasure, 2 onLayout, 1 onDraw, theoretically perform three measurements, but due to the measurement optimization strategy, onMeasure will not be executed for the third time.
api23-21: Execute onMeasure 3 times, onLayout 2 times, and onDraw 1 time. The forceLayout flag is strangely set to true, resulting in no measurement optimization.
api19-16: Execute onMeasure twice, onLayout twice, and onDraw once. The reason is that only the performMeasure and forceLayout flags in the measureHierarchy will be executed in the first performTransversals, and they are strangely set to true, resulting in no measurement optimization.
In short, the root cause of this phenomenon is that the performTranversal function will be executed twice in the measurement process of View.
See the specific reason:
Link: https://www.jianshu.com/p/733c7e9fb284
It is difficult to say how many times it was called, but it has to be explained by printing the diary.

Why do you say this. Because when you customize the calculation logic, it is likely to cause data confusion or memory jitter due to calling twice in onmesure, so when doing a custom view rewriting function, print logs several times first. Continue later.
For example, do this in the following situation:
when you create a new Arraylist, it is best to put it on the attribute, and then clear it every time onmesure. First of all, if you don’t clear, your data will be messed up because of multiple calls to onmesure, and if you choose to directly go to new Arraylist in onmesure, it will cause memory jitter. Memory jitter is the continuous creation and release of memory.

public class MyLayout extends ViewGroup {

private List<Integer>list= new ArrayList<>();

    public MyLayout(Context context) {
        super(context);
    }

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
          list.clear();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        
    }
}

Guess you like

Origin blog.csdn.net/weixin_43836998/article/details/85452894