关于Android的自定义view相关的知识不管是面试还是我们深入学习都是不可避免要接触的知识模块,我们用到的一些开源控件随随便便都是上百上千行的代码,顿时让我们感觉自定义view很难,但是当我们读了类似知名博主郭霖,任玉刚一类的讲解自定义view方面的文章,我们就会感觉其实自定义view也不是很难,下面本文将会讲解这方面几个难点,希望对你们有所帮助。
首先我们应该了解Android的事件分发机制,本文只做一个精简的概括,具体源码还需要读者去看相关博文分析。我们需要知道的是当我们点击界面的一个view控件button的时候它内部其实发生了这样的调用关系的,当我们同时注册onTouch和onClick事件,控件点击事件的一定会去顶级父view调用dispatchTouchEvent方法这个是所有点击事件的前提。
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
通过这个方法我们可以知道,里面的三个条件同时为true的时候才不会去调用onTouchEvent方法,实际情况下它也不会去执行onClick方法了,所以我们可以断定onClick方法的执行与onTouchEvent有着密不可分的关系,onTouchEvent主要源码:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
//-------------------
break;
case MotionEvent.ACTION_DOWN:
//-------------------
break;
case MotionEvent.ACTION_CANCEL:
//-------------------
break;
case MotionEvent.ACTION_MOVE:
//-------------------
break;
}
return true;
}
return false;
当控件可点击的时候进入判断条件,当我们抬起手指离开屏幕的时候会走进去MotionEvent.ACTION_UP这个case中,其实我们注册的onClick方法就是在这个逻辑里面调用的,第一点:这里所有的case都返回了true,说明只有每一个MotionEvent动作只有返回true才能继续执行下一个,当我们在其中任意一个返回了false就破坏了这个规则,导致后面的动作就执行不到了。第二点:当我们控件默认是不可点击的时候,说明进入不了switch事件,直接返回了false,所以它的up cancel move事件包括click事件都不会执行了,这两个知识点很重要。
对于ViewGroup其实本质差不多,点击ViewGroup布局上的view一定是先去调用ViewGroup的 dispatchTouchEvent方法,然后找到该view调用它的dispatchTouchEvent方法,步骤如上所叙。ViewGroup的dispatchTouchEvent方法源码:
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
//---------------------------
//循环遍历找到该view并判断该view是不是当前点击的view然后调用它的dispatchTouchEvent方法
if (child.dispatchTouchEvent(ev)) {
mMotionTarget = child;
return true;
}
}
/**
* ----------------------------
*/
return super.dispatchTouchEvent(ev);
/**
* ----------------------------
*/
disallowIntercept 可以设置此ViewGroup是否屏蔽,默认是false,子View可以调用requestDisallowInterceptTouchEvent(true)来修改这个属性值,第二个参数是对onInterceptTouchEvent方法返回值取反操作。重要结论:对于viewgroup如果子view没有被disallowIntercept onInterceptTouchEvent两个条件拦截,并且dispatchTouchEvent返回true那么便不会执行自己的touch和click事件。
说完Android的事件分发机制后我们需要了解scrollTo 以及 scrollBy方法区别以及Scroller类的作用,首先我们要知道scroll移动的时候它的坐标轴:
看着是不是和我们所了解的坐标系不一样呢,至于Google的工程师为什么这么设置就不得而知了,scrollTo 以及 scrollBy有什么区别呢?我们看看源码:
protected int mScrollX; //该视图内容相当于视图起始坐标的偏移量 X轴方向
protected int mScrollY; //该视图内容相当于视图起始坐标的偏移量 Y轴方向
//返回值
public final int getScrollX() {
return mScrollX;
}
public final int getScrollY() {
return mScrollY;
}
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy方法的实现还是调用scrollTo方法,我们可以这样理解:scrollBy是将视图内容继续偏移(x , y)个单位,scrollTo表示当前视图内容偏移至(x , y)坐标处,这就是他们的区别。一个形象的比喻就相当于你慢慢走到家和你变身瞬移到家是一个意思的。听到瞬移什么?一个view到另一个位置是瞬移,这相当于没有过渡动画的ios系统,用户体验是不是太差了,是的这样我们ui的交互体验确实不好,那怎么解决呢?不用担心google为我们开发者提供了Scroller类来解决这个问题,它的几个重要方法:
//开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx , startY+dy)处
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
这个效果就很类似我们使用的viewpager,viewpager滑动的效果应该也是通过这个方法去控制的吧,当然那它的自动滑动的效果是怎么实现的呢,这儿我们可以通过 computeScroll()这个方法来控制,这个方法会在view进行绘制的时候调用的,并且是一个空方法需要自己去实现,在这个computeScroll()方法里面我们需要用Scroller实例获得它的偏移量并且手动scrollTo到该偏移量并自带有动画效果。至于viewpager的手势效果,我们可以重新它的onTouchEvent方法,通过MotionEvent那几个动作来判断滑动动画的启动暂停左右滑动包括停留原屏打开下一屏等等….
再者我们还需要对Android控件的测量有所认识,具体就是view 的 measure 流程,View 的 layout 过程,View 的 draw 过程,对于这方面的细述博文太多了而且很多讲的很详细,本文就不详细介绍了,主要总结作用,我们需要理解控件的测量规格MeasureSpec这个类,他是一个32位的int类型,它的高2位代表测量模式Mode,低30位代表测量大小Size。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
//0x3代表十六进制3,所以MODE_MASK表示1100 0000 0000 0000 0000 0000 0000 0000
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//0000 0000 0000 0000 0000 0000 0000 0000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//0100 0000 0000 0000 0000 0000 0000 0000
public static final int EXACTLY = 1 << MODE_SHIFT;
//1000 0000 0000 0000 0000 0000 0000 0000
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
其MeasureSpec的创建遵循下表中的规则(图片来自任玉刚博客):
字view的测量规格由父控件的父控件的测量规格和子控件的布局LayoutParams里的尺寸共同决定的,具体规则如上。在获取获取 View 的宽高有一种方法就是手动测量我们可以这样写
//比如宽高都是 120px ,如下 measure :
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(120, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(120, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
//宽高都是wrap_content 由于size是后30位表示的所以最大值就是(1 << 30) - 1
int widthMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
//宽高含有match_parent 由于无法确定具体大小所以测不了
测量完成后会调用onLayout方法帮助子孩子们确定好位置,坐标点一般是这四个值0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,所以此时getWidth()方法和getMeasuredWidth() 得到的值就是相同的,基本上按照这种规则写getWidth和getMeasuredWidth获得的测量的值是一样的,如果我们传入的值不按这个规则比如说改变该view起点坐标或者top button传入自定义的常量参数就会导致getWidth和getMeasuredWidth测量的值不一样。
最后我们还要知道Android常用的一些动画,View Animation 、Drawable Animation 、Property Animation,
1.View Animation也是我们平时很多书籍所说的Tweened Animation(有人翻译为补间动画)。View Animation分为4大类:AlphaAnimation,RotateAnimation,ScaleAnimation,TranslateAnimation,3.Property Animation分别对应透明度,旋转,大小,位移四种变化。 2.Drawable Animation这种动画(也叫Frame动画、帧动画)其实可以划分到视图动画的类别,专门用来一个一个的显示Drawable的resources,就像放幻灯片一样。属性动画只对Android 3.0(API 11)以上版本的Android系统才有效,这种动画可以设置给任何Object,包括那些还没有渲染到屏幕上的对象。这种动画是可扩展的,可以让你自定义任何类型和属性的动画。其次如果你也对Paint与Canvas绘制弧线(arcs)、填充颜色(argb和color)、 Bitmap、圆(circle和oval)、点(point)、线(line)、矩形(Rect)、图片(Picture)、圆角矩形 (RoundRect)、文本(text)、顶点(Vertices)、路径(path)等等深入了解的话,那么恭喜你可以说完全掌握了自定义控件的写法,O(∩_∩)O哈!