1.Android 高级UI<一>之自定义view绘制原理 (2024精华版)

目录:

  1. view的绘制流程

  2. ViewGroup的绘制流程?

  3. 为什么不能在子线程更新UI?

  4. Activity View Window ViewRootImp之间的关系

5. 三大方法详解测量

  1. 如何获取 View 宽高?

  2. inflate和setContentView原理解析

8. 自定义view之如何自定义View?

9. 自定义view优化

10.自定义View你做过哪些东西?

1. view的绘制流程

view绘制流程.jpg
1.1 问题: View的绘制流程路口是哪? View从哪个方法开始绘制的?第一次绘制的消息是怎么发出来的?

入口:setContentView开始(错误)!

应该是ActivityThread开始,在里面的handleResumeActivity()方法

问题: View的绘制流程是从Activity的哪个生命周期方法开始执行的????

View的绘制流程是从Activity的 onResume 方法开始执行的

OnResume的作用,也就是HandleResumeActivity的作用,把window和Acrtivity绑定。

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    if (r == null) {
        return;
    }
    final Activity a = r.activity;
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
1.2 具体的流程
先AMS------>
      APP进程的ActivityThread---->(handleResumeActivity)
               WMS------>(windowImp实现)
                     ActivityThread(handleResumeActivity)
                               Window(addView)
                                       WindowManager(addView)
                                                 WindowManagerGlobal(addView)
ViewRootImp的setView()----ViewRootImp的RequestLayout()---performTraversals()3个方法

1). View系统的绘制流程会从ViewRootImpl的performTraversals()方法中开始,performTraversals()的意思是:执行遍历,Traversals:遍历的意思

performTraversals会分别调用 performMeasure, performLayout,performDraw

而这三个方法,我想你应该能猜到,他们会启动onMesure,onLayout,onDraw方法

2). ViewRoot中包含了窗口的总容器DecorView,ViewRoot中的performTraversal()方法会依次调用decorView的measure、layout、draw方法,从而完成view树的绘制。

3). measure()方法,layout(),draw()三个方法主要存放了一些标识符,来判断每个View是否需要再重新测量,布局或者绘制

View树的绘制是一个递归的过程,从ViewGroup一直向下遍历,直到所有的子view都完成绘制

总结: View的整个绘制流程可以分为以下三个阶段:

  • measure: 判断是否需要重新计算View的大小,需要的话则计算;每个View的控件的实际宽高都是由父视图和本身视图决定的
  • layout: 判断是否需要重新计算View的位置,需要的话则计算;
  • draw: 判断是否需要重新绘制View,需要的话则重绘制。
  • measure()、layout()、draw(),其内部又分别包含了onMeasure()、onLayout()、onDraw()三个子方法。

2. ViewGroup的绘制流程?

通常ViewGroup情况下不需要绘制,因为本身就没什么可绘制的东西,如果不是指定了ViewGroup的背景颜色,那么ViewGroup的onDraw()方法都不会被调用。

ViewGroup会使用dispatchDraw()方法来绘制其子View,其过程同样是通过遍历所有子View,并调用子View的绘制方法来完成绘制工作。

ViewGroup测量源码分析如下:

有2个方法:一个是measureChild()和measureChildWithMargins()

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

getChildMeasureSpec();

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            resultMode = MeasureSpec.EXACTLY;
        break;
    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

总结viewGroup的测量流程:会遍历, 调用measureChildren方法,根据父类的测量模式得到期望子类的测量模式,然后调用view的onmesure方法。

重点: View的测量大小和父类的的模式也是有关系的

先说结论:默认情况下,当父布局为 wrap_content 或者 match_parent 时,无论子 view(view 或者 viewgroup) 是wrap_content 还是 match_parent,最终的效果都是 match_parent。也就是 子 view 会占据父布局中剩下的所有空间。

具体的4种情况

  1. .如果当前控件的宽高是确切值就用这个值,否则由父元素决定。

2). 如果子控件是match_parenter。那么就把父元素的大小给子控件。

3). 如果子控件是wrap_content。父元素的大小给子控件

4). 如果父类是Wrap_content,就按自己的背景大小或者最小值来显示,如果父view有限制,就按父view给的尺寸来显示**

3. 为什么不能在子线程更新UI?

竟然崩溃了,那问题来了,到底子线程能不能更新Ui呢?

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

最重要的方法来了,mThread线程是主线程,Thread.currentThread()是当前线程,即我们运行的子线程

假如当前更新UI不在主线程,就会导致CalledFromWrongThreadException异常

由此可见,每一次刷新View都会调用ViewRootImp的checkThread()方法去检测是否在主线程

问题: 但是google为什么要这样去设计呢?

(1)如果在不同的线程去控制用一个控件,由于网络延时或者大量耗时操作,会使UI绘制错乱,出了问题也很难去排查到底是哪个线程更新时出了问题;

(2)主线程负责更新,子线程负责耗时操作,能够大大提高响应效率

(3)UI线程非安全线程,多线程进行并发访问有可能会导致内存溢出,降低硬件使用寿命;且非线程安全不能加Lock线程锁,否则会阻塞其他线程对View的访问,效率就会变得低下!

4. Activity View Window ViewRootImp之间的关系

view_wm.jpg

Activity : 不负责控制视图,只是控制生命周期和处理事件,真正控制视图的是Window,Activity中含有一个Window,Window才是真正代表一个窗口

Window : 安卓中所有的视图都是通过Window来呈现的,比如Activity、Dialog、Toast; Window的添加、删除和更改是通过WindowManager来实现的, 视图的承载器,内部持有DecorView,而DecorView是View的根布局,Window是一个抽象类,真正的实现类是PhoneWindow,

问题: Window的作用是什么?

Activity和View的中间作用,为了解藕!

PhoneWindow:**有个内部类DecorView,通过其来加载R.layout.activity_main。Window通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图的绘制及其他交互

DecorView

是FrameLayout的子类,是android的根视图,相当于顶级View,一般来说内部包含竖直方向LinearLayout,在linearlayout中含有三部分,上面是ViewStub,延迟加载的视图,中间是标题栏,下面是内容栏,就是我们熟悉的android.R.id.content

ViewRoot

所有View的绘制及事件分发交互都是通过它来进行的,有个真正的实现类ViewRootImpl,它是链接WindowManagerService和DecorView的纽带,View的测量,布局,绘制都是通过它来实现的,所以我们常见的事件分发真正的过程是

几者的关系: 硬件->ViewRootImpl->DecorView->PhoneWindow->Activity->ViewGroup->View

问题: ViewRootImp是什么时候创建的? Activity是如何创建view的? DecorView是什么时候创建的?

ViewRootImp创建: onResume()的时候

问题: phoneWindow是什么时候创建的?

ActivityThread---------->performLaunchActivity

           Activity------->attach()   ::   在Activity的attach()的时候 
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);

DecorView的创建: onCreate()在setContentView的时候,但是DecorView和Activity没有什么联系?直到onResume()方法。他们相互绑定

问题: DecorView是什么时候绑定activity的?

onResume()的时候

问题: Activity是如何创建view的?

1).Activity----->setContentView     
         PhoneWindow 
               DecorView-------AddView(ViewRootImp)
                      ViewRootImpViewRootImpl------------>setView()
                             binder进程
                                   WMS底层
                                         SurfaceFling
view结构.jpg

5. 三大方法详解测量

5.1 layout
5.2 测量:

问题: View与ViewGroup的onMeasure的方法有什么去区别?

ViewGroup没有onMeasure.它要么就要是用的View的方法

View有onMeasure方法和measure方法

ViewGroup一半都是调用MeasureChildren()方法

答:简单来讲就是:ViewGroup除了测量自身,还需要测量子View的大小,

ViewGroup中提供了对子View的测量方法: measureChildren(int widthMeasureSpec, int heightMeasureSpec),

在measureChildren中遍历所有子View,调用measureChild((int widthMeasureSpec, int heightMeasureSpec),

在measureChild中调用了View的measure(int widthMeasureSpec, int heightMeasureSpec)方法,最终执行View的onMeasure()方法,让子View测量自身大小。

5.2.2 问题: Android的wrap_content是如何计算的?

通过父类测量,设置测量模式,然后自己测量方法,通过Measurepec转成精确的值

5.2.3 .说下Measurepec这个类

一个类,把模式和值封装在一起,这个类在view中。是是一个类,不是一个常量

测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)

作用:然后测量转化为具体的数值。

`1).EXACTLY

精确值模式,当控件的layout_width和layout_height属性指定为具体数值或match_parent时。

match_parent:属于哪种?EXACTLY

EXACTLY =01000000000000000000000000000000

2).AT_MOST

最大值模式,当空间的宽高设置为wrap_content时。

wrap_content:属于哪种?AT_MOST

AT_MOST =10000000000000000000000000000000

3).UNSPECIFIED

UNSPECIFIED=00000000000000000000000000000000

未指定模式,View想多大就多大,通常在绘制自定义View时才会用。

决定因素:值由子View的布局参数LayoutParams和父容器的MeasureSpec值共同决定。具体规则见下图:

layoutPararms和父类的MeasureSpec决定子类的策略measureSpec。`

结论:子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的

问题: .控件的宽高和哪些因素有关系?

测量逻辑:

 如果子视图对于 Measure 得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次measure。

比如,父视图可以先根据未给定的 dimension 去测量每一个子视图, 如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的 大小再次对子视图进行 measure。

measure 过程传递尺寸的两个类

  1. .ViewGroup.LayoutParams (View 自身的布局参数)

  2. .MeasureSpecs 类(父视图对子视图的测量要求)

当不需要绘制 Layer 的 时候第二步和第五步会跳过。因此在绘制的时候,能省的 layer 尽可省,可以

  • View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
  • 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。
  • ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。
  • 只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

总结:MeasureSpce的mode有三种:EXACTLY, AT_MOST,UNSPECIFIED,除却UNSPECIFIED不谈,其他两种mode:

当父布局是EXACTLY时,子控件确定大小或者match_parent,mode都是EXACTLY,子控件是wrap_content时,mode为AT_MOST;

当父布局是AT_MOST时,子控件确定大小,mode为EXACTLY,子控件wrap_content或者match_parent时,mode为AT_MOST。

所以在确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用

5.2.4 为什么你的自定义View wrap_content不起作用?
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize; // 这里的spectSize代表match_parent
            break;
        }
        return result;
    }

如果自定义view设置为wrap_content, 那么它的宽和高和父类一样! 默认是父view的AT_MOST,也就是剩余最大空间, 导致失效!

问题: 如何解决这一问题?

我们只需要在自定义View中的onMeasure自定义我们的宽高,然后通过setMeasuredDimension写回即可

引申:直接继承View的自定义View需要重写onMeasure()并设置wrap_content时的自身大小,否则效果相当于macth_pather:原因是因为:源代码里面有

举一个实例:extend view ,不写onMeasure ,会怎么显示

如果自定义View没有重写onMeasure函数,就看viewGroup里面的源码

extent viewGroup 不写onMeasure 不写onMeasure,不可以显示

模式是由父布局和自己决定的。

比如:父亲是wrapcontent,就算子布局是match_parent,这个时候测量模式还是at_most

      父亲是match_parent,子布局是match_parent,这个时候测量模式还是exactly

其他结论

1.继承view,子布局即使在xml中有宽高,不写onMeasure,可以显示

2.View测量的时候,默认是EXACTLY模式,你不重写OnMeasure方法,即使设置wrap_content属性,他也是填充父容器。(不是viewgroup)

3.继承ViewGroup,子布局即使在xml中有宽高,不写onMeasure,不可以显示 ,必须重写ononMeasure

4.继承LinearyLayout,子布局即使在xml中有宽高,不写onMeasure,可以显示

一句话总结:

5.2.5 面试官:为什么要重写onmesure方法:

如果不对上面的onMeasure()方法进行重写,无论是warp_content还是match_parentSpecSzie的大小都是窗口的大小,也就是自定义View所在的布局大小,而布局的宽高一般默认是match_parent,显示在屏幕上就是DecorView中的ContentView的大小。

5.3 draw:

5.3.1.invalidate()和postInvalidate()和requestlayout的使用与区别

1). invalidate 会先找到父类去走绘制流程,最终遍历所有相关联的 View ,触发它们的 onDraw 方法进行绘制(ondraw)

在UI线程调用。view的invalidate会导致当前view被重绘,由于mLayoutRequested为false,不会导致onMeasure和onLayout被调用,

触发它们的 onDraw

2). invalidate()得在UI线程中被调动,在工作者线程中可以通过Handler来通知UI线程进行界面更新。

  而postInvalidate()在工作者线程中被调用。

3). requestLayout:有布局需要发生改变,需要调用requestlayout方法,如果只是刷新动画,则只需要调用invalidate方法。

这时候适合调用这个方法requestLayout()。 requestLayout调用onMeasure和onLayout,不一定调用onDraw

requestLayout源码分析:

view----->requestLayout
               viewGroup-------->requestLayout
                         viewRootImp------->requestLayout
                                    viewRootImp-------->performTraversals
5.3.2. android中View的GONE和INVISIBLE的原理

1).visible:3个方法都执行

2).INVISIBLE:执行2个方法,不执行onDraw方法 可以的都宽高

3).Gone: 一个方法都不会执行,得不到宽和高

源码分析:都会执行invalideat。然后3大绘制流程,然后根据标志位判断!

void setFlags(int flags, int mask) {
    if (((mViewFlags & FOCUSABLE_AUTO) != 0)
            && (changed & (FOCUSABLE_MASK | CLICKABLE)) != 0) {
        mViewFlags = (mViewFlags & ~FOCUSABLE) | newFocus;
        focusableChangedByAuto = (old & FOCUSABLE) ^ (newFocus & FOCUSABLE);
        changed = (changed & ~FOCUSABLE) | focusableChangedByAuto;
    }
    /* Check if the FOCUSABLE bit has changed */
    if (((changed & FOCUSABLE) != 0) && ((privateFlags & PFLAG_HAS_BOUNDS) != 0)) {
    final int newVisibility = flags & VISIBILITY_MASK;
    if (newVisibility == VISIBLE) {
            invalidate(true);

5.3.3 View绘制流程中,onMeasure方法将执行几次

最少执行2次,最多执行4次。如果界面为弹窗,那么在activity的setContentView方法中会设置WindowManager.LayoutParams的width为WRAP_CONTENT,这时在预测量阶段会有协商的过程,这时最多会测量3次。默认情况下预测量阶段只会测量一次。因此onMeasure方法最少执行2次,最多执行4次。

6. 如何获取 View 宽高?

6.1 3种方法:

1).通过View.post ()。获取宽和高

2). attachToWindow()

3). 通过监听得到的

问题: 在onResume中是否可以测量宽高???? 可用通过View.post () 获取得到

6.2 .getWidth() ( getHeight())与 getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)有什么区别?
  • getWidth() / getHeight():获得View最终的宽 / 高
  • getMeasuredWidth() / getMeasuredHeight():获得 View测量的宽 / 高

总结:

他们的值大部分时间都是相同的,但意义确是根本不一样的,

  • 首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。

  • getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,

而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。

// 获得View测量的宽 / 高
  public final int getMeasuredWidth() {  
      return mMeasuredWidth & MEASURED_SIZE_MASK;  
      // measure过程中返回的mMeasuredWidth
  }  
  public final int getMeasuredHeight() {  
      return mMeasuredHeight & MEASURED_SIZE_MASK;  
      // measure过程中返回的mMeasuredHeight
  }  
// 获得View最终的宽 / 高
  public final int getWidth() {  
      return mRight - mLeft;  
      // View最终的宽 = 子View的右边界 - 子view的左边界。
  }  
  public final int getHeight() {  
      return mBottom - mTop;  
     // View最终的高 = 子View的下边界 - 子view的上边界。
  } 

问题: Activity中使用Handler.post和View.post有什么区别?

其实,当View已经attach到了window,两者是没有区别的,都是调用UI线程的Handler发送runnable到MessageQueue,最后都是由handler进行消息的分发处理。

但是如果View尚未attach到window的话,runnable被放到了ViewRootImpl#RunQueue中,最后runnable的处理不是通过MessageQueue,而是ViewRootImpl自己在下一个performTraversals到来的时候执行。

总结:

1). 当View已经attach到window,不管什么线程, 调用View#post 和 调用Handler#post效果一致

2). 当View尚未attach到window,主线程调用View#post发送的runnable将在下一次performTraversals到来时执行,而非主线程调用View#post发送的runnable将无法被执行。

可以通过在主线程调用View#post发送runnable来获取下一次performTraversals时视图树中View的布局信息,如宽高。

如果调用View#post方法的线程对象被GC-Root引用,则发送的runnable将会造成内存泄漏。

在 onResume() 中 handler.post(Runnable) 是无法正确的获取不到 View 的真实宽高

在 onResume 中handler.post 在 View.post 后面为什么执行反而在前面;
通过上面第2点和点3点分析可以知道View.post的在后面performTraversals中被执行,而handler.post在performTraversals之前就被执行

View.post() 为什么能够获取到 View 的宽高 ?

runable不会立马执行,在ViewRootImpl的performTraversals中调用了host的dispatchAttachedToWindow,然后mRunQueue执行的时机是在view的dispatchAttachedToWindow中

里面发送了一个消息,仅仅保存起来。
测量后回调用dispatchAttachedToWindow

源码分析:
可以看出 onResume() 方法在 addView() 方法前调用
重点关注:onResume() 方法所处的位置,前后都发生了什么?
从上面总结的流程看出,onResume() 方法是由 handleResumeActivity 触发的,而界面绘制被触发是因为 handleResumeActivity() 中调用了wm.addView(decor, l);

7. inflate和setContentView原理解析

7.1 setContentView源码分析
@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}

1)首先初始化mDecor,即DecorView为FrameLayout的子类。就是我们整个窗口的根视图了。 调mWindow.setContentView(subDecor);

2). 进行了xml文件的解析 .注意:后面源码都是有所改变的。

  1. 通过findViewById获取Activity布局文件该存放的地方(窗口修饰布局文件中id为content的FrameLayout)

问题:supre.onCreate()放在setContentView之后可以吗?

不行。还有些没用准备好!

protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_mock);

7.2 inflate里面3个参数是什么意思?设计有什么用?

自定义view和系统view。自己的view需要自己设置LayoutParams

ublic View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
   
            } else {
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    if (DEBUG) {
                        System.out.println("Creating params from root: " +
                                root);
                    }
                    // Create layout params that match root, if supplied
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }

问题: Activity#setContentView中的xml文件是如何转化成View并显示到Activity中的。

8. 自定义view之如何自定义View?

8.1 自定义view套路:

1).构造方法,自定义属性(可配置)

2).onMesure ,如果是继承view,要写。如果是继承button,这种就不要写了

3).ondraw()

  1. .onTouch()

android onmesure里面是否要重写super.onMeasure()

8. 1. 2 问题: 自定义View为什么有3个构造函数?问题:哪个构造函数一定要有?

第一个:new 出来

第二个:xml中,findviewById 可以查看源码:layoutFlate,里面通过反射实现的。(context ,attr)

第三个:主题用到

问题:哪个构造函数一定要有?

第二个,和反射有关,可以看setcontentview的源码!!!

8.1.3 . margin和padding

如果不手动设置支持padding属性,那么padding属性在自定义View中是不会生效的?

protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
       // 获取传入的padding值
       final int paddingLeft = getPaddingLeft();
       final int paddingRight = getPaddingRight();
       final int paddingTop = getPaddingTop();
       final int paddingBottom = getPaddingBottom();

android 源码分析padding替代margin======3大布局性能比较的时候

用padding替代margin。只是针对RelativeLayout。

RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,尽量使用padding代替margin。

8.1.4 自定义View执行invalidate()方法,为什么有时候不会回调onDraw()

1).调用view.invalidate(),会触发onDraw和computeScroll()。前提是该view被附加在当前窗口上

2). 自定义一个ViewGroup,重写onDraw。onDraw可能不会被调用,原因是需要先设置一个背景(颜色或图)。

  表示这个group有东西需要绘制了,才会触发draw,之后是onDraw。

 因此,一般直接重写dispatchDraw来绘制viewGroup,自定义一个ViewGroup,dispatchDraw会调用drawChild

自定义一个view时,重写onDraw。调用view.invalidate(),会触发onDraw和computeScroll(),前提是该view被附加在当前窗口,也就是说view必须是当前Window上面的。

8.2 一般自定义ViewGroup的写法:

第一种方式:

1).自定义属性(可配置)很少写

2).onMesuare() for循环测量子view。根据子view计算自己的宽和高

3).onlayout()

4).不会ondraw,如果要实现用DispatchDraw()

  1. .一般不继承viewGroup.而是linearyLayout,或者viewPager();

一定要注意是,当模式是MeasureSpec.EXACTLY时,我们就不必要设定我们计算的大小了,因为这个大小是用户指定的,我们不应更改。

但当模式是MeasureSpec.AT_MOST时,也就是说用户将布局设置成了wrap_content,我们就需要将大小设定为我们计算的数值,因为用户根本没有设置具体值是多少,需要我们自己计算。

第二种方式:

  ViewGroup去会管理其子View,包括管理负责子View的显示大小。当ViewGroup的大小为wrap_content,ViewGroup就需要对子View进行遍历,以便获得所有子View的大小,从而来决定自己的大小。

  在其他模式下会通过具体的指定值来设置自身的大小。
   1).VIewGroup在测量时会通过遍历所有子View,从而调用子View的Measure方法来获得每个子View的测量结果,前面所说的对View的测量,就是在这里进行的。
    2).当子View测量完毕后,就需要将子View放到合适的位置,这个过程就是View的Layout过程。ViewGroup在执行Layout过程时,同样是遍历来调用子View的Layout方法,并指定其具体显示的位置,从而来决定其布局位置。
  在自定义ViewGroup时,通常会去重写onLayout()方法来控制其子View显示位置的逻辑。同样,如果需要支持wrap_content属性,那么它必须要还要重写onMeasure()方法,这点与View是相同的。

8. 3 自定义控件实现方式:

1).自定义组合控件
2).继承已有控件
3).继承View(构造函数里获取自定义属性)

  1. .继承ViewGroup

备注:

而一些自定义View,现在大厂中必备的技能,频率非常非常之高,可能每个人对自定义View的理解也不尽相同,又说可能说有三种可能说有多种,其实在大厂中用的最多的那种叫做自定义组合View。

因为大厂里不建议你直接去画一个View,即自己去绘制的这个控件,而更建议去使用原生的或者现成的优质View,即能去组合就去组合,所以这也体现了自定义组合View的重要。

自定义组合View因为可以把自己的逻辑封装到一起,这样可以即简洁又高效。其实有一些部门可能会专门去画一些View,封装这些View以及框架等,或者说有一些专门的人就做一些纯绘制View。

这样会避免一些自己画的可能兼容性和通用性不是很好,也可能还会隐藏其他的BUG,所以说大厂中很不建议自己就画一个View(直接继承View和ViewGroup),因此说自定义组合View成了一个大厂的基本的一个要求

9. 自定义view优化

9. 1 自定义view效率高于xml定义吗?说明理由

自定义view效率高于xml定义:

1、少了解析xml。

2.、自定义View 减少了ViewGroup与View之间的测量,包括父量子,子量自身,子在父中位置摆放,当子view变化时,父的某些属性都会跟着变化。

计算一个view的嵌套层级

3. 绘制优化:自定义视图允许你完全控制视图的绘制过程。你可以手动优化绘制流程,避免不必要的绘制操作,减少绘制的重复工作,以提高性能。在某些情况下,使用自定义视图可以避免不必要的视图层级嵌套,从而减少了布局层次的复杂性,进而提高了性能。
9.2 View生命周期相关方法:

onFinishInflate() 当View中所有的子控件均被映射成xml后触发

onMeasure( int , int ) 确定所有子元素的大小

onLayout( boolean , int , int , int , int ) 当View分配所有的子元素的大小和位置时触发

onSizeChanged( int , int , int , int ) 当view的大小发生变化时触发

onDraw(Canvas) view渲染内容的细节

onKeyDown( int , KeyEvent) 有按键按下后触发

onKeyUp( int , KeyEvent) 有按键按下后弹起时触发

onTrackballEvent(MotionEvent) 轨迹球事件

onTouchEvent(MotionEvent) 触屏事件

onFocusChanged( boolean , int , Rect) 当View获取或失去焦点时触发

onWindowFocusChanged( boolean ) 当窗口包含的view获取或失去焦点时触发

onAttachedToWindow() 当view被附着到一个窗口时触发

onDetachedFromWindow() 当view离开附着的窗口时触发,Android123提示该方法和 onAttachedToWindow() 是相反的。

onWindowVisibilityChanged( int ) 当窗口中包含的可见的view发生变化时触发

综上所述:View 的关键生命周期为 [改变可见性] --> 构造View --> onFinishInflate --> onAttachedToWindow --> onMeasure --> onSizeChanged --> onLayout --> onDraw --> onDetackedFromWindow

onAttachedToWindow是在第一次onDraw前调用的。也就是我们写的View在没有绘制出来时调用的,但只会调用一次。

onDetachedFromWindow:销毁资源(既销毁view)之后调用。

9.3 .veiw状态的保持

view是先父view测量子view,等子view测量完,再测量自己

首先Activity 被意外终止时,Activity 会调用onSaveInstanceState 去保存数据,然后Activity 会委托Window 去保存数据,接着Window 在委托它上面的顶级容器去保存数据。顶级容器是一个ViewGroup,一般来说它很可能是DecorView。

最后顶层容器再去一一通知它的子元素来保存数据,这样整个数据保存过程就完成了。可以发现,这是一个典型的委托思想,上层委托下层,父容器去委托子元素去处理一件事情,这种思想在Android 中有很多应用,比如:View 的绘制过程,事件分发等都是采用类似的思想。

既然View的状态是基于它的ID存储的 , 因此如果一个VIew没有ID,那么将不会被保存到container中。没有保存的支点(id),我们也无法恢复没有ID的view的状态,因为不知道这个状态是属于哪个View的。

这里需要注意一个细节:想要保存View的状态,需要在XML布局文件中提供一个唯一的ID(android:id),

如果没有设置这个ID的话,View控件的onSaveInstanceState是不会被调用的。

要保存view的状态,至少有两点需要满足:

view要有id

要调用setSaveEnabled(true)

都用SparseArray来存储的

9.4 .如何优化自定义View?针对measure和draw 有什么优化的地方吗? 多个view 加载卡顿怎么办

view绘制原理方面

1)减少在onDraw 里面大量计算和对象创建和大量内存分配。

2).应该尽量少用invalidate()次数。或是调用带四种参数不同类型的invalidate()

3.invalidate调用和requestLayout调用

4.使用硬件加速,GPU 硬件加速可以带来性能增加。

5.状态保存与恢复

过渡绘制方面

1).去掉window 的默认背景

2).减少重叠区域canvas.clipRect()

draw()优化

1).里面有7个步骤: 第2---3,不一一定执行。可以改善优化

9.5 自定义View 如何考虑机型适配 ?

实现原理

以一个特定的宽度尺寸的设备为参考,在View加载的过程中,根据当前设备的实际像素换算出目标像素,再作用到控件上。

通常UI给我们的设计稿只有一种像素的标准,例如720 * 1280。 例如在设计稿上有一个控件宽度为屏幕尺寸的一半,即360px,假设我们真机的屏幕尺寸为1080 * 1920像素,我们再布局中设置控件尺寸也为360px,则显示为屏幕的三分之一。但是我们想要效果仍然是显示屏幕的一半。即宽度应该设置为540px,这里的540px是如何得到的呢,这就是通过真实屏幕尺寸与参考设计稿的尺寸比例来得到的,即:(1080/720)*360 = 540。

o 合理使用warp_content,match_parent

o 尽可能的是使用RelativeLayout

o 针对不同的机型,使用不同的布局文件放在对应的目录下,android 会自动匹配。

o 尽量使用点9 图片。

o 使用与密度无关的像素单位dp,sp

o 引入android 的百分比布局。

o 切图的时候切大分辨率的图,应用到布局当中。在小分辨率的手机上也会有很好的显示效果。

9. 6 . 自定义view怎么局部刷新?只刷新部分ui,比如上面有视频播放,下面按钮不断缩小这种。

invalidate(Rect dirty)

invalidate(int l, int t, int r, int b)

不过,随着系统版本的迭代,官方已标记这两个方法为 @Deprecated,推荐直接用 invalidate() 方法。

10.自定义View你做过哪些东西?

1).跑步首页,记录步数的进度条+文字(view)

2).流式布局 (viewGroup)

  1. . 收音机滑动卡尺 (滑动view)

4).自定义TabLayout (组合控件)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容