Android -- 每日一问:如何实现自定义View?
经典回答
回忆一下,你去面试时常被问到的自定义 View 方面的问题是那些。有没有:
- invalidate 和 postInvalidate 方法的区别?
- 自定义 View 的绘制流程?
- View 的 Touch 事件分发流程?
因为在实际的工作中并不是每个人都会涉及UI的实现,所以有些人没有做过自定义 View 并不能否决这个人在 Android 开发上的能力,包括会问你这方面问题的面试官也可能并没有自定义 View 的经验。所以很多面试中,一般也就是问问如上面的那些机制方面的问题,看面试者是否有一个正确的认识。但如果一个人说他精通自定义View的话,不妨从细节上检验一下他。
在说我怎么检验面试者的自定义View水平之前,先来一道老外的面试题,大家不妨先自己试着敲一下代码看能不能实现。
效果界面如下:
这一道题其实把我们刚刚提到的面试常被问到的那些机制问题都涉及到了,你很容易就能查找和了解并可以很好的回答上那几个基础问题,但是你能做出这个自定义View吗?
这就是机制和现实的差距!你有必要了解机制(基础),但了解并不等于会了,会的过程需要一定的积累,只有融会贯通了才能轻而易举地完成这个老外的面试题。
我们简单剖析一下,这个可左右滑动的View中是一组子View组成的,每个子View(圆盘carousel)可以自定界面,但背景的样式是相似(每组背景只是颜色不一样),有点ListView中的Adapter味道,对滑动的过渡是有要求的,这个其实是更真实操作Touch的样例(没有人想看生硬的滑动效果)。那么我们要能实现上面的效果,需要注意以下几个点:
-
ViewGroup的布局(计算一行能显示的圆盘数量和大小)
圆盘View的效果(背景和半透明过渡)
左右列表和BaseAdapter
圆盘View的滑动(需要自动回弹,保证滑动后要有一个圆盘处于中间位置) -
View & ViewGroup
自定义View(有时我们也可以叫自定义UI或者自定义UI组件),从实现或者分类上我觉得可以分为三类: -
直接继承View
继承自ViewGroup
对现有组件的扩展(如继承自TextView)
第3种方式是比较容易的,因为父组件常常帮我处理了绘制、分发和Touch等事件,我们只需要加入一些特别的功能就行。比如继承自ImageView实现图片圆角显示。
第1和第2种方式,需要我们对前面提到的那些机制问题有一定的了解,也要搞清楚View和ViewGroup在哪些方面有什么区别。
ViewGroup继承自View,是一种特殊的View,可以理解成一种View的容器,它可以装其他的View或其他的ViewGroup。
ViewGroup需要控制子View如何布局,所以必须实现onLayout(在ViewGroup中是抽像方法)。
ViewGroup可以通过onInterceptTouchEvent拦截当前事件,再决定是否分发给子View处理。
ViewGroup默认是不会调用onDraw方法的,如果需要重绘容器的背景需要在构造函数调用setWillNotDraw(false)。
除此之外,要熟悉一些概念(或者类):Canvas, Paint, Matrix,Path & PathMeasure,贝塞尔曲线,SufaceView & OpenGL等等。是不是觉得概念太多了,是的,没有实际做过,你一定会这样想。让你为了面试去准备这么多东西,貌似也起不到多大的作用,面试官不一定会问(除非指明了招专门做自定义UI的开发)。而且就算面试官问了一个你准备好的主题,但他换一种说法问你,如果你只是准备过但并没有掌握的话,也不一定能答得上来。就拿PathMaesure来说,做为面试官一般不会直接问你:“这个PathMeasure有什么方法或者主要是做什么用的啊?”
而往往会用实例来看你的实现思路,例:“如何实现下图的这个箭头图标(png图片)围绕园周运动的自定义View”。如果你的做法不是使用PathMeasure,那么我也会很有兴趣想听听你的思路和实现方法。
灵活使用PathMeasure的常见的两个方法可以轻松的实现很多神奇的效果。
getSegment:截取整个Path的片段;
getPosTan:获取路径上的坐标点和对应点的切线坐标。
使用PathMeasure实现的核心代码:
path = new Path();
path.addCircle(0, 0, 200, Path.Direction.CW); //添加一个圆的path
pathMeasure = new PathMeasure(path, false);
float[] pos = new float[2];
float[] tan = new float[2];
pathMeasure.getPosTan(pathMeasure.getLength() * value, pos, tan);
float degress = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); // 计算箭头图片的旋转角度
如果面试者不了解PathMeasure的话,也可以问一下图片圆角(或称矩形圆角)或者圆形头像的实现方式,这个也是一个很常见的功能,效果如下图:
把原图直接做成圆角外,常见有三种方式实现:
- 使用 Xfermode 混合图层;
- 使用 BitmapShader;
- 通过裁剪画布区域实现指定形状的图形(ClipPath)
你的朋友是不是也在准备面试呢?你可以把今天的题目分享给好友,或许你可以帮到他。