Android自定义View解析之视图状态及视图重绘(二)

1、最常用的几种视图状态

1. enabled

表示当前视图是否可用。可以调用setEnable()方法来改变视图的可用状态,传入true表示可用,传入false表示不可用。它们之间最大的区别在于,不可用的视图是无法响应onTouch事件的。

2. focused

表示当前视图是否获得到焦点。有两种方法可以让视图获得焦点,即通过键盘的上下左右键切换视图,以及调用requestFocus()方法。而现在的Android手机几乎都没有键盘了,因此基本上只可以使用requestFocus()这个办法来让视图获得焦点了如果返回true说明获得焦点成功,返回false说明获得焦点失败。一般只有视图在focusable和focusable in touch mode同时成立的情况下才能成功获取焦点,比如说EditText。

3. window_focused

表示当前视图是否处于正在交互的窗口中,这个值由系统自动决定,应用程序不能进行改变。

4. selected

表示当前视图是否处于选中状态。一个界面当中可以有多个视图处于选中状态,调用setSelected()方法能够改变视图的选中状态,传入true表示选中,传入false表示未选中。

5. pressed

表示当前视图是否处于按下状态。可以调用setPressed()方法来对这一状态进行改变,传入true表示按下,传入false表示未按下。通常情况下这个状态都是由系统自动赋值的,也可以自己调用这个方法来进行改变。

2、视图重绘原理

每当视图的状态有发生改变的时候,就会回调View的drawableStateChanged()方法,源码如下:

protected void drawableStateChanged() {
    final Drawable d = mBackground;
    if (d != null && d.isStateful()) {
        d.setState(getDrawableState());
    }

    if (mStateListAnimator != null) {
        mStateListAnimator.setState(getDrawableState());
    }
}

如上所示,首先是将mBackground赋值给一个Drawable对象,那么这个mBackground是什么?

经过查找得知mBackground在setBackgroundDrawable方法中被赋值,而setBackgroundDrawable方法又在setBackground方法中被调用,而setBackground方法又在setBackgroundResource(int)中调用,那让我们看看setBackgroundResource代码吧:

public void setBackgroundResource(int resid) {
	if (resid != 0 && resid == mBackgroundResource) {
		return;
	}

	Drawable d = null;
	if (resid != 0) {
		d = mContext.getDrawable(resid);
	}
	setBackground(d);

	mBackgroundResource = resid;
}

如上所示,mBackground其实是被d实例赋值的,而drawbale实例d是通过Resource的getDrawable()方法将resid转换成了一个Drawable对象,即其实质就是一个布局文件。

而我们在布局文件中通过android:background属性指定的selector文件,效果等同于调用setBackgroundResource()方法。也就是说drawableStateChanged()方法中的mBackground对象其实就是我们指定的selector文件。

在该方法里给mBackground赋值后,会调用getDrawableState()方法来获取视图状态的集合,代码如下:

public final int[] getDrawableState() {
//判断当前视图的状态是否发生了改变
//desc:如果没有改变就直接返回当前的视图状态
//如果发生了改变就调用onCreateDrawableState()方法来获取最新的视图状态
	if ((mDrawableState != null)
			&& ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) {
		return mDrawableState;
	} else {
		mDrawableState = onCreateDrawableState(0);
		mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY;
		return mDrawableState;
	}
}

在得到了视图状态的数组之后,就会调用DrawablesetState()方法来对状态进行更新。接下来我们看看setState(),如下所示:

public boolean setState(final int[] stateSet) {
//判断视图状态的数组是否发生了变化
	if (!Arrays.equals(mStateSet, stateSet)) {
		mStateSet = stateSet;
		return onStateChange(stateSet);
	}
	return false;
}

在该方法里面又会去调用Drawable的的onStateChange()方法,而该方法只是简单返回了一个false,并没有任何逻辑处理,结果真的是这样的吗?其实不然。因为mBackground对象是通过一个selector文件创建出来的,而通过这种文件创建出来的Drawable对象其实都是一个StateListDrawable实例,因此这里调用的onStateChange()方法实际上调用的是StateListDrawable中的onStateChange()方法,代码如下:

protected boolean onStateChange(int[] stateSet) {
	int idx = mStateListState.indexOfStateSet(stateSet);
	if (DEBUG)
		android.util.Log.i(TAG, "onStateChange " + this + " states "
				+ Arrays.toString(stateSet) + " found " + idx);
	if (idx < 0) {
		idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
	}
	if (selectDrawable(idx)) {
		return true;
	}
	return super.onStateChange(stateSet);
}

这里会先调用indexOfStateSet()方法来找到当前视图状态所对应的Drawable资源下标,然后在调用selectDrawable()方法并将下标传入,在这个方法中就会将视图的背景图设置为当前视图状态所对应的那张图片。

前面我们已经知道,任何视图的显示都需经过科学的绘制流程,那么能肯定背景图的绘制也必定是在draw()方法中完成的,那么为什么selectDrawable()方法能够控制背景图的改变呢接下来我们研究下视图的重绘流程。

3、视图重绘流程

我们知道视图会在Activity加载完成之后自动绘制到屏幕上,但在与Activity进行交互的时候通常要求动态更新视图,比如改变视图的状态、以及显示或隐藏某个控件等。那在这个时候,之前绘制出的视图其实就已经过期了,此时我们就应该对视图进行重绘

调用视图的setVisibility()setEnabled()setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现。View的源码中会有数个invalidate()方法的重载和一个invalidateDrawable()方法,当然它们的原理都是相同的因为他们最终都需调用invalidateInternal()方法

让接下来就让我们看看invalidateInternal()方法吧,源码如下:

void invalidateInternal(int l, int t, int r, int b,
		boolean invalidateCache, boolean fullInvalidate) {
	if (mGhostView != null) {
		mGhostView.invalidate(true);
		return;
	}
//1、第一步 判断当前View是否需要重绘
//查看该方法知道如果View是不可见的且没有执行任何动画,就认为不需要重绘了
	if (skipInvalidate()) {
		return;
	}
//2、第二步 进行透明度的判断,并给View添加一些标记位
	if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
			|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
			|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
			|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
		if (fullInvalidate) {
			mLastIsOpaque = isOpaque();
			mPrivateFlags &= ~PFLAG_DRAWN;
		}

		mPrivateFlags |= PFLAG_DIRTY;

		if (invalidateCache) {
			mPrivateFlags |= PFLAG_INVALIDATED;
			mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
		}

		// Propagate the damage rectangle to the parent view.
		final AttachInfo ai = mAttachInfo;
		final ViewParent p = mParent;
		if (p != null && ai != null && l < r && t < b) {
			final Rect damage = ai.mTmpInvalRect;
			damage.set(l, t, r, b);
			p.invalidateChild(this, damage);
		}

		// Damage the entire projection receiver, if necessary.
		if (mBackground != null && mBackground.isProjected()) {
			final View receiver = getProjectionReceiver();
			if (receiver != null) {
				receiver.damageInParent();
			}
		}

		// Damage the entire IsolatedZVolume receiving this view's shadow.
		if (isHardwareAccelerated() && getZ() != 0) {
			damageShadowReceiver();
		}
	}
}

如上所示,首先invalidate()这个方法判断当前View是否需要重绘,接着会调用ViewParentinvalidateChild()方法,这里的ViewParent其实就是当前视图的父视图,因此会调用到ViewGroupinvalidateChild()方法代码如下:

public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;

    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
      .........      

        do {
            View view = null;
            if (parent instanceof View) {
                view = (View) parent;
            }

            if (drawAnimation) {
                if (view != null) {
                    view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                } else if (parent instanceof ViewRootImpl) {
                    ((ViewRootImpl) parent).mIsAnimating = true;
                }
            }

            // If the parent is dirty opaque or not dirty, mark it dirty with the opaque
            // flag coming from the child that initiated the invalidate
            if (view != null) {
                if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                        view.getSolidColor() == 0) {
                    opaqueFlag = PFLAG_DIRTY;
                }
                if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                    view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
                }
            }

            parent = parent.invalidateChildInParent(location, dirty);
            if (view != null) {
                // Account for transform on current parent
                Matrix m = view.getMatrix();
                if (!m.isIdentity()) {
                    RectF boundingRect = attachInfo.mTmpTransformRect;
                    boundingRect.set(dirty);
                    m.mapRect(boundingRect);
                    dirty.set((int) (boundingRect.left - 0.5f),
                            (int) (boundingRect.top - 0.5f),
                            (int) (boundingRect.right + 0.5f),
                            (int) (boundingRect.bottom + 0.5f));
                }
            }
        } while (parent != null);
    }
}

invalidateChild()中有一个while循环,当ViewParent不等于空的时候这个while循环会不断地获取当前布局的父布局,并调用它的invalidateChildInParent()方法,在ViewGroupinvalidateChildInParent()方法中主要是来计算需要重绘的矩形区域,当循环到最外层的根布局后,就会调用ViewRootinvalidateChildInParent()方法了

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    invalidateChild(null, dirty);
	return null;
}

invalidateChildInParent()中仅仅是调用了invalidateChild()方法

public void invalidateChild(View child, Rect dirty) {
	checkThread();
	if (LOCAL_LOGV)
		Log.v(TAG, "Invalidate child: " + dirty);
	mDirty.union(dirty);
	if (!mWillDrawSoon) {
		scheduleTraversals();
	}
}

而该方法用调用了scheduleTraversals

public void scheduleTraversals() {
	if (!mTraversalScheduled) {
		mTraversalScheduled = true;
		sendEmptyMessage(DO_TRAVERSAL);
	}
}

方法中又去调用sendEmptyMessage()方法,并传入了一个DO_TRAVERSAL参数。了解Android异步消息处理机制会知道,任何一个Handler都可以调用sendEmptyMessage()方法来发送消息,并且在handleMessage()方法中接收消息,ViewRoot的类是继承自Handler的,也就是说这里调用sendEmptyMessage()方法来发送消息,会在ViewRoot的handleMessage()方法中接收到。那看看handleMessage方法咯:

public void handleMessage(Message msg) {
    switch (msg.what) {
    case DO_TRAVERSAL:
        if (mProfile) {
            Debug.startMethodTracing("ViewRoot");
        }
        performTraversals();
        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
        break;
    ......
}

会发现该方法有调用了performTraversals()方法,到了视图绘制的入口这个时候一切的一切在明白不过了吧。可见调用视图的invalidate()方法后最终一定会走到performTraversals()方法中,然后重新执行绘制流程

接下来就可以解释selectDrawable()方法中到底做了什么才能够控制背景图的改变了。

让我们回顾下selectDrawable()方法,他的代码如下:

public boolean selectDrawable(int idx) {
	if (idx == mCurIndex) {
		return false;
	}
	......

	if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
		if (mAnimationRunnable == null) {
			mAnimationRunnable = new Runnable() {
				@Override
				public void run() {
					animate(true);
					invalidateSelf();
				}
			};
		} else {
			unscheduleSelf(mAnimationRunnable);
		}
		// Compute first frame and schedule next animation.
		animate(true);
	}

	invalidateSelf();

	return true;
}

如上所示,在这个方法中一最终会调用invalidateSelf()方法,如下所示:

public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

invalidateSelf这个方法中的Callback接口的回调实例又调用了invalidateDrawable()方法,而View又是Callback接口的实现类,所以其实就是调用View中的invalidateDrawable()方法,前面说了该方法最终会执行invalidateInternal现在是不是一目了然了。

这里强调下,invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果希望视图的绘制流程可以完地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。这个方法中心思想invalidate()差不多的,这里也就不再详细进行分析了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值