Android View 绘制流程之四:绘制流程触发机制

系列文章:
Android View 绘制流程之一:measure测量
Android View 绘制流程之二:layout布局
Android View 绘制流程之三:draw绘制
Android View 绘制流程之四:绘制流程触发机制

View系统的核心部分就是测量、布局、绘制三大过程,我们平时调用的setVisibility、requestLayout、invalidate等方法看似都会去导致View系统的重新遍历,本文就是讲述这些方法如何发起了一个View重新遍历的请求,以及该请求如何被处理的、在哪里被处理的。

  1. 首先需要知道,ViewRootImpl类作为View的最顶层的一个ViewParent,管理着整个View系统,当然也就包括了测绘流程

  2. ViewRootImpl类中的scheduleTraversals方法是发起一次View系统的遍历的调用方法,它内部会使用handleR向主线程MessageQueue中发起一个消息,且是一个同步消息,即不进行完整个application不会运作(具体如何发的还不是很理解,后续会学习),最终执行该消息时,会调用其Callback(一个Runnable对象)的run方法,执行doTraversal方法,该方法内部调用performTraversals方法,即是开启整个View遍历的起点

  3. 我们平时的一些改变view的状态、可见性、增加删除包括请求重绘、请求布局等操作,都会直接或间接的调用到scheduleTraversals方法发起遍历请求,最终调用performTraversal方法进行遍历

  4. 整个测绘流程实质是:在一次消息处理中,可以任意改变任意view的各种状态,各种参数,然后统一发起一次遍历请求,在这个请求执行时,根据这些已经更新的状态、参数重新测绘view系统

下面就分几个常见的方面来说明下平时的一些操作如何发起了请求,并在最后说明performTraversal的运行流程。

一.View状态的改变(包括Background的drawable状态改变)

View系统给View提供了许多状态,常见的比如enable、focused、pressed、selected,我们可以在drawable里定义,最终系统将drawable文件转换为一个StateListDrawable对象,目的是为了改变view在不同状态下的背景效果,在使用时,有的状态需要我们自己手动改变,有的是系统在一些时机自己改变,当改变后,会根据新的state选择相应的drawable进行重绘背景,先介绍一下这些常见状态:

  1. enable:是否可用,我们可以通过setEnable()方法改变其状态,而且其默认的效果是使view变为50%透明

  2. focused:是否具有焦点,一个窗口只能有一个View获得焦点,具体焦点部分介绍在下面会说

  3. pressed:是否按下,我们一般使用的view如果定义了点击事件,按下时都会有背景的改变,因为系统层面以及调用了setPressed方法替我们改变了其状态,当然我们也可以自己进行控制

  4. selected:是否选中,这是一个完全交由用户定义控制的状态,系统不会处理,加入我们需要自定义的一种状态,就可以通过setSelected来控制该状态,不过要注意的是,如果setSelected(false)的话,会将pressed状态也置为false

下面来介绍一下改变这几个状态(focus相关下一小节单说)的方法及其如何刷新View的:
在这里插入图片描述

1.setEnable()

public void setEnabled(boolean enabled) {
   
    if (enabled == isEnabled()) return;//如果没有改变则不做处理

    setFlags(enabled ? ENABLED : DISABLED, ENABLED_MASK);//改变了view的flag,这里用不到

    refreshDrawableState();//根据新state刷新drawable,该方法最后会说

    invalidate(true);//由于默认的操作是将view变为50%透明度,所以仍然需要刷新重绘
	...
}

流程很简单,就是改变flag,刷新drawable,再重绘变为50%透明度即可,refreshDrawableState方法就是刷新drawable的方法,下面会说到。

2.setPressed()

public void setPressed(boolean pressed) {
   
    final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);//pressed状态是用标志位的方式记录的,所以用位运算判断是否改变了状态
	//更新标志位
    if (pressed) {
   
        mPrivateFlags |= PFLAG_PRESSED;
    } else {
   
        mPrivateFlags &= ~PFLAG_PRESSED;
    }
	//如果改变了状态则需要刷新drawable
    if (needsRefresh) {
   
        refreshDrawableState();
    }
    dispatchSetPressed(pressed);//通知子view状态改变
}
//ViewGroup的dispatchSetPressed方法
protected void dispatchSetPressed(boolean pressed) {
   
    final View[] children = mChildren;
    final int count = mChildrenCount;
    for (int i = 0; i < count; i++) {
   
        final View child = children[i];
        if (!pressed || (!child.isClickable() && !child.isLongClickable())) {
   //规则就是:如果父view已经点击了,那么子view如果可以点击(有点击或者长按的功能)的话,就不能响应点击事件了
            child.setPressed(pressed);
        }
    }
}

和enable差不多,流程也是改变状态刷新drawable,只不过多了一个通知子view的步骤

3.setSelected()

public void setSelected(boolean selected) {
   
    //noinspection DoubleNegation
    if (((mPrivateFlags & PFLAG_SELECTED) != 0) != selected) {
   //也是用标志位来记录状态
        mPrivateFlags = (mPrivateFlags & ~PFLAG_SELECTED) | (selected ? PFLAG_SELECTED : 0);//更新状态
        if (!selected) resetPressedState();//如果selected置为false,那么需要重置pressed状态
        invalidate(true);//重绘
        refreshDrawableState();//刷新drawable
        dispatchSetSelected(selected);//通知子view,这里就是使每个子view都设置成该selected状态即可(即父view选中则子view都选中,父view不选中则都不选中)
        ...
    }
}

4.refreshDrawableState()

上述几个方法都是主要都是调用refreshDrawableState方法来刷新drawable,下面来看看这个方法

 public void refreshDrawableState() {
   
    mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;标识drawable的state需要更新
    drawableStateChanged();//更新drawable的state

    ViewParent parent = mParent;
    if (parent != null) {
   
		//此步是通知父view要刷新drawable的state,原理:如果父view设置了一个叫addStatesFromChildren的属性,那么所有其子view拥有的状态都包含在其父view中,也就是说父view和其子view背景同步,做法就是直接调用父view的refreshDrawableState方法
        parent.childDrawableStateChanged(this);
    }
}
 
protected void drawableStateChanged() {
   
    final int[] state = getDrawableState();//根据view的flag判断其拥有的状态位,并将其打成数组返回

    final Drawable bg = mBackground;
    if (bg != null && bg.isStateful()) {
   
        bg.setState(state);//调用drawable的setState方法更新states
    }

    ...
}
 
public boolean setState(final int[] stateSet) {
   
    if (!Arrays.equals(mStateSet, stateSet)) {
   //状态改变了则需要更新
        mStateSet = stateSet;
        return onStateChange(stateSet);//由Drawable子类重写该方法处理更新回调
    }
    return false;
}
 
protected boolean onStateChange(int[] stateSet) {
   
    final boolean changed = super.onStateChange(stateSet);

    int idx = mStateListState.indexOfStateSet(stateSet);//找到新的状态对于的在该drawable的状态列表里的index
    if (idx < 0) {
   
        idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);//默认的状态index
    }

    return selectDrawable(idx) || changed;//选择drawable进行更新
}
 
public boolean selectDrawable(int idx) {
   
    if (idx == mCurIndex) {
   //没有改变则不做处理
        return false;
    }

    ...

    if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
   
        final Drawable d = mDrawableContainerState.getChild(idx);//找到对应的drawable
        mCurrDrawable = d;
        mCurIndex = idx;
        ...
    } else {
   
        mCurrDrawable = null;
        mCurIndex = -1;
    }

    ...

    invalidateSelf();//核心方法,刷新drawable自己的部分,最基本的View会调用到invalidateDrawable方法

    return true;
}
 
//View的invalidateDrawable方法
public void invalidateDrawable(@NonNull Drawable drawable) {
   
    if (verifyDrawable(drawable)) {
   
        final Rect dirty = drawable.getDirtyBounds();
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;

        invalidate(dirty.left + scrollX, dirty.top + scrollY,
                dirty.right + scrollX, dirty.bottom + scrollY);//实质就是调用invalidate方法刷新drawable的区域
        rebuildOutline();
    }
}

由代码可知,更新drawable流程就是根据view当前状态列表,找到对应的新的drawable,然后最终调用invalidate刷新drawable的这部分区域即可,本质也是调用invalidate

这里需要注意的是那个setAddStatesFromChildren方法,它将设置父view与子view的背景联动,实质就是在构建ViewGroup的drawableState时,会将子view的所有drawableState合并在一起交给父view,并在子view刷新drawable时通知父view即可

还有一个是setDuplicateParentStateEnabled方法,该方法是指该view的状态变化与父view一样,与上面这个相反

二.View焦点的改变

首先需要知道的是一个窗口最多只能有一个view处于拥有焦点的状态,而我们一般通过requestFocus来请求获取焦点,以及setFocusable和setFocusableInTouch来设置是否可获取焦点、是否可在触摸状态(窗口有触摸状态还有键盘输入状态)时获取焦点,还有一些辅助方法比如findFoucs、getFocusChild和hasFoucs,下面先来介绍一下这些辅助方法,再去看如何获取焦点的:
在这里插入图片描述

1.findFoucs()

//判断view是否拥有焦点
public boolean isFocused() {
   //通过标志位来记录focus状态
    return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
//ViewGroup的findFoucs方法
public View findFocus() {
   
    if (isFocused()) {
   //如果是自身拥有焦点则返回自身
        return this;
    }

    if (mFocused != null) {
   //mFocused是该ViewGroup保存的拥有焦点的直接子view(真正焦点view可能在更深层的view)
        return mFocused.findFocus();//调用mFocused的该方法递归寻找
    }
    return null;//没有焦点view
}
//View的findFoucs方法
public View findFocus() {
   
    return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;//直接判断view有没有该标志即可
}

该方法可知:

  1. 返回的是具体的拥有焦点的一个view

  2. 可能没有找到拥有焦点的view(该view不是或者该ViewGroup没有焦点view)

2.getFocusedChild()

该方法是ViewGroup的方法,直接返回上面说过的mFocused对象,即该view的包含焦点view的直接子view,有可能为null,下面会说该变量的赋值更新

3.hasFocus()/hasFocusable()

hasFocus()方法是判断view是否有焦点,有两种可能,一种是其自身有焦点,一种是其包含焦点view(mFocused != null)

hasFocusable()方法是判断view是否有focusable属性,或者ViewGroup是否包含有focusable属性的子view

4.isFocusable()/isFocusableInTouchMode()

这两个方法就是判断view是否有focusable和focusableInTouchMode属性,属性可以通过xml或者代码设置

这几个常用的焦点相关的方法介绍完后,来看看View系统的寻找焦点流程:

5.requestFocus()请求获取焦点

requestFocus方法是请求焦点的方法,其返回值是boolean,代表是否该view或者该view的子view获取到了焦点;对于ViewGroup,会重载该方法,进而判断是自己获取焦点还是子view获取焦点;而对于View来说会直接调用requestFocusNoSearch来尝试使自己获取焦点;

下面来看看相关方法

//View的requestFocusNoSearch方法
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
   
    // 前提条件
	//需要focusable属性为true,且必须为VISIBLE状态才行
    if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
            (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
   
        return false;
    }

	//如果在touch模式下,focusableInTouch属性必须为true才行
    if (isInTouchMode() &&
        (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
   
           return false;
    }

	//循环遍历parent,如果有parent阻止子类获取焦点则不行(setDescendantFocusability方法设置ViewGroup的拦截焦点属性,下面会说)
    if (hasAncestorThatBlocksDescendantFocus()) {
   
        return false;
    }
	//前提条件满足,该view可以获取焦点,则返回true代表获取成功,当然还要调用handleFocusGainInternal去更新focus状态以及全局的focus相关数据
    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;
}
//View的handleFocusGainInternal方法
void handleFocusGainInternal(@FocusRealD
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android的视图绘制Android应用程序中的重要部分,它涉及到将用户界面元素绘制到屏幕上。以下是Android视图绘制的基本流程: 1. 触发绘制:当应用程序启动、布局发生变化或者手动调用 `invalidate()` 方法时,会触发视图绘制。 2. 测量布局:在绘制之前,Android会测量每个视图的大小。这个过程称为“测量布局”。测量布局是为了确定每个视图在屏幕上的位置和大小。 3. 布局:一旦测量完成,Android会根据视图的测量结果进行布局,确定每个视图在屏幕上的位置。 4. 绘制:布局完成后,Android会调用每个视图的 `draw()` 方法进行绘制。在 `draw()` 方法中,视图会绘制自己的内容,包括背景、文字、图片等。 5. 绘制层次:视图的绘制按照层次结构进行,即从父视图到子视图的顺序。父视图会先绘制自己,然后再绘制子视图。 6. 递归绘制:当父视图绘制完成后,它会递归地调用子视图的 `draw()` 方法,依次完成整个视图树的绘制过程。 7. 绘制缓存:为了提高绘制性能,Android使用了绘制缓存。绘制缓存可以将视图的绘制结果保存起来,在下次绘制时直接使用缓存,而不需要重新执行绘制操作。 总结来说,Android的视图绘制过程包括测量布局、布局、绘制绘制缓存。通过这个过程,Android应用程序可以将用户界面元素绘制到屏幕上,实现丰富多样的交互效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值