注:本文中的代码是《开发艺术探索》书中的源码,特别感谢这本书的作者任玉刚先生,真的从其中学到了很多,特此感谢。
先说下实现的效果,就是在这个布局中的所有控件在点击时会有一个阴影,手指不离开会从手指的地方出现一个更深的圆形阴影,不断蔓延到覆盖整个空间。没有办法截图,如果有魅族手机可以看一下,资讯界面的所用的布局就是这样的。
事件分发
先找到被点击view
public class RevealLayout extends LinearLayout implements Runnable {
private static final String TAG = "DxRevealLayout";
private static final boolean DEBUG = true;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int mTargetWidth;//子view的宽度
private int mTargetHeight;//子view的高度
private int mMinBetweenWidthAndHeight;//子view的宽度与高度的最小值
private int mMaxBetweenWidthAndHeight;//子view的宽度与高度的最大值
private int mMaxRevealRadius;//半径最大值
private int mRevealRadiusGap;//半径单位变化值
private int mRevealRadius = 0;//初始半径
private float mCenterX;//中心点X
private float mCenterY;//中心点Y
private int[] mLocationInScreen = new int[2];//父view的左上角的坐标数组
private boolean mShouldDoAnimation = false;
private boolean mIsPressed = false;
private int INVALIDATE_DURATION = 40;
private View mTouchTarget;//触摸的view
private DispatchUpTouchEventRunnable mDispatchUpTouchEventRunnable = new DispatchUpTouchEventRunnable();
public RevealLayout(Context context) {
super(context);
init();
}
public RevealLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public RevealLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setWillNotDraw(false);
mPaint.setColor(getResources().getColor(R.color.reveal_color));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
this.getLocationOnScreen(mLocationInScreen);
}
先看一下注释,这部分内容基本可以理解重写了父类的构造方法,初始化一些需要的变量。接下来就是重点了。
先重写父类的dispatchTouchEvent方法来确定事件的分发。
/**
* 重写父类的dispatchTouchEvent方法来决定事件的分发
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
View touchTarget = getTouchTarget(this, x, y);
if (touchTarget != null && touchTarget.isClickable() && touchTarget.isEnabled()) {
mTouchTarget = touchTarget;
initParametersForChild(event, touchTarget);
//TODO???不是很懂
//查资料意思是用于在工作线程重绘view
postInvalidateDelayed(INVALIDATE_DURATION);
}
} else if (action == MotionEvent.ACTION_UP) {
mIsPressed = false;
postInvalidateDelayed(INVALIDATE_DURATION);
//创建一个执行view点击事件的任务对象
mDispatchUpTouchEventRunnable.event = event;
//把runnable对象放到主线程取执行
postDelayed(mDispatchUpTouchEventRunnable, 200);
return true;
} else if (action == MotionEvent.ACTION_CANCEL) {
//就是指当前的事件序列没有执行完的时候执行,被其他的view抢走了
mIsPressed = false;
postInvalidateDelayed(INVALIDATE_DURATION);
}
return super.dispatchTouchEvent(event);
}
在这其中有两个陌生的方法分别作一下说明,getTouchTarget()根据坐标获得点击位置的veiw,initParametersForChild()用于对初始化的成员进行赋值。
/**
* 根据坐标获得子view
* @param view parentview
* @param x
* @param y
* @return
*/
private View getTouchTarget(View view, int x, int y) {
View target = null;
//返回所有能被触摸的view
ArrayList<View> TouchableViews = view.getTouchables();
for (View child : TouchableViews) {
if (isTouchPointInView(child, x, y)) {
target = child;
break;
}
}
return target;
}
其中又掉用了一个方法isTouchPointInView(),用于判断坐标是否在view的范围内
/**
* 根据坐标判断是否在view 中
* @param view child view
* @param x 点击的x
* @param y 点击的Y
* @return
*/
private boolean isTouchPointInView(View view, int x, int y) {
int[] location = new int[2];
view.getLocationOnScreen(location);
int left = location[0];
int top = location[1];
int right = left + view.getMeasuredWidth();
int bottom = top + view.getMeasuredHeight();
if (view.isClickable() && y >= top && y <= bottom
&& x >= left && x <= right) {
return true;
}
return false;
}
赋值方法
/**
* 对一系列的属性进行赋值
* @param event 事件
* @param view 被触摸的view
*/
private void initParametersForChild(MotionEvent event, View view) {
mCenterX = event.getX() ;
mCenterY = event.getY() ;
mTargetWidth = view.getMeasuredWidth();
mTargetHeight = view.getMeasuredHeight();
mMinBetweenWidthAndHeight = Math.min(mTargetWidth, mTargetHeight);
mMaxBetweenWidthAndHeight = Math.max(mTargetWidth, mTargetHeight);
mRevealRadius = 0;
mShouldDoAnimation = true;
mIsPressed = true;
mRevealRadiusGap = mMinBetweenWidthAndHeight / 8;
int[] location = new int[2];
view.getLocationOnScreen(location);
//子view的左上角横坐标减去父view的左上角横坐标
//得到子view相对于父view在水平方向的距离
int left = location[0] - mLocationInScreen[0];
//得到点击点相对于子view左边的水平距离
int transformedCenterX = (int)mCenterX - left;
//返回点击点在子view中距离水平两边的最大值
mMaxRevealRadius = Math.max(transformedCenterX, mTargetWidth - transformedCenterX);
}
这样事件的分发就执行完了,接下来就是需要的重绘了。
重绘
根据获得点击事件的view来进行重绘实现点击的效果。
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (!mShouldDoAnimation || mTargetWidth <= 0 || mTouchTarget == null) {
return;
}
if (mRevealRadius > mMinBetweenWidthAndHeight / 2) {
mRevealRadius += mRevealRadiusGap * 4;
} else {
mRevealRadius += mRevealRadiusGap;
}
this.getLocationOnScreen(mLocationInScreen);
int[] location = new int[2];
mTouchTarget.getLocationOnScreen(location);
int left = location[0] - mLocationInScreen[0];
int top = location[1] - mLocationInScreen[1];
int right = left + mTouchTarget.getMeasuredWidth();
int bottom = top + mTouchTarget.getMeasuredHeight();
canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawCircle(mCenterX, mCenterY, mRevealRadius, mPaint);
canvas.restore();
if (mRevealRadius <= mMaxRevealRadius) {
postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
} else if (!mIsPressed) {
mShouldDoAnimation = false;
postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
}
}
之前的代码都能看懂的话这部分应该也不难,就是对属性的理解和判断,实现画一个圆,经常看到的一个方法就是postInvalidateDelayed(),用于延时刷新 view,在源码中查了查,查到最后也不是很懂最后的方法的实现就是view的刷新机制,之后会继续关注的。到这里这个RevealLayout就解释的差不多了,有些地方我也不是很懂,比如canvas的save和restore方法的作用。mShouldDoAnimation 的作用等。不过如果上面我写的内容明白了的话,这个布局也就理解的可以了。(之前说些事件分发的还没写,哎,最近好忙啊)。贴一些代码查找的过程和发现。
下面的内容是对postInvalidateDelayed()方法的追踪,跟上文没太大的关系,就是自己记录一下,个人比较喜欢查找源码,就好比在畅游于知识的海洋,最后碰见了一条大鱼。
首先是view中的postInvalidateDelayed()方法
/**
* <p>Cause an invalidate of the specified area to happen on a subsequent cycle
* through the event loop. Waits for the specified amount of time.</p>
*
* <p>This method can be invoked from outside of the UI thread
* only when this View is attached to a window.</p>
*
* @param delayMilliseconds the duration in milliseconds to delay the
* invalidation by
* @param left The left coordinate of the rectangle to invalidate.
* @param top The top coordinate of the rectangle to invalidate.
* @param right The right coordinate of the rectangle to invalidate.
* @param bottom The bottom coordinate of the rectangle to invalidate.
*
* @see #invalidate(int, int, int, int)
* @see #invalidate(Rect)
* @see #postInvalidate(int, int, int, int)
*/
public void postInvalidateDelayed(long delayMilliseconds, int left, int top,
int right, int bottom) {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.obtain();
info.target = this;
info.left = left;
info.top = top;
info.right = right;
info.bottom = bottom; attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info, delayMilliseconds);
}
}
大家稍微看一下就行,就是对一些信息进行封装到了attachInfo中。重点是最后一句attachInfo.mViewRootImpl.dispatchInvalidateRectDelayed(info,delayMilliseconds);
调用了一个ViewRoot的实现类的dispatchInvalidateRectDelayed()方法;
让我们看看它里面做了什么?
public void dispatchInvalidateRectDelayed(AttachInfo.InvalidateInfo info,
long delayMilliseconds) {
final Message msg = mHandler.obtainMessage(MSG_INVALIDATE_RECT, info);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
可以看到它往消息队列中发了一个消息。继续跟踪下,看看这个消息执行的是什么?
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
case MSG_INVALIDATE_RECT:
final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
info.target.invalidate(info.left, info.top, info.right, info.bottom);
info.recycle();
break;
看case的第二个,执行了一个info.target.invalidate()方法。info.target是什么哪?我怀疑是一个view对象,取view中查查看有没有这方法啊?
/**
* Mark the area defined by the rect (l,t,r,b) as needing to be drawn.
* The coordinates of the dirty rect are relative to the view.
* If the view is visible, {@link #onDraw(android.graphics.Canvas)}
* will be called at some point in the future. This must be called from
* a UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.
* @param l the left position of the dirty region
* @param t the top position of the dirty region
* @param r the right position of the dirty region
* @param b the bottom position of the dirty region
*/
public void invalidate(int l, int t, int r, int b) {
if (skipInvalidate()) {
return;
}
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) ||
(mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID ||
(mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED) {
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags |= PFLAG_DIRTY;
final ViewParent p = mParent;
final AttachInfo ai = mAttachInfo;
//noinspection PointlessBooleanExpression,ConstantConditions
if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
if (p != null && ai != null && ai.mHardwareAccelerated) {
// fast-track for GL-enabled applications; just invalidate the whole hierarchy
// with a null dirty rect, which tells the ViewAncestor to redraw everything
p.invalidateChild(this, null);
return;
}
}
if (p != null && ai != null && l < r && t < b) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
final Rect tmpr = ai.mTmpInvalRect;
tmpr.set(l - scrollX, t - scrollY, r - scrollX, b - scrollY);
p.invalidateChild(this, tmpr);
}
}
}
还真有,看一下都是些什么东西啊,if判断,赋值,之后执行了 p.invalidateChild(this, tmpr);这个方法,这p是什么啊?点一下方法,就会跳到这里。
/**
* All or part of a child is dirty and needs to be redrawn.
*
* @param child The child which is dirty
* @param r The area within the child that is invalid
*/
public void invalidateChild(View child, Rect r);
这是ViewParent接口中的一个抽象方法,到这里,跟踪就暂时进行不下去了,因为我们并不知道需要到哪个实现了这个接口的实现类中去查看实现的方法,考虑一下,方法中的child是谁?是不是我们最开始确定的被点击的view,那这个view的父类是谁哪?对,就是RevealLayout,现在回到我们的RevealLayout中去看看有没有这个方法。会发现并没有实现接口,那RevealLayout的父类LinearLayout那,我们看下。
@RemoteView
public class LinearLayout extends ViewGroup {
public abstract class ViewGroup extends View implements ViewParent, ViewManager
好的现在我们找到了这个invalidateChild()方法的实现对象了,去ViewGroup 中看看吧。
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*/
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
这个方法里有很多的判定,赋值之类的,一时间我是看不过来,有能力的可以继续查一下。主要的我就是想找到在哪里调用了RevealLayout中的dispatchDraw这个方法,不过也没有找到,我之前说的大鱼就是这个方法,还有一个跟它在一起的
/**
* Don't call or override this method. It is used for the implementation of
* the view hierarchy.
*
* This implementation returns null if this ViewGroup does not have a parent,
* if this ViewGroup is already fully invalidated or if the dirty rectangle
* does not intersect with this ViewGroup's bounds.
*/
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
这两个方法实现了对viewgroup中的子view进行刷新操作,这种代码块实在是太多了,只能通过注释来稍微理解一点。不过还是没有找到我想要的,因为view的刷新肯定是要实时进行的,也就是说dispatchDraw这个方法会执行很多次,直到结束,那么在刷新的时候哦是谁在执行这个方法?在什么时候执行?有感兴趣的可以留言或私信相互交流,之后也会关注一下view刷新机制流程方面的信息。那么这篇博客到这就结束了。