ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
#### 一、闲扯篇 自定义View,很多初学Android的童鞋听到这么一句话绝逼是一脸膜拜!因为在很多初学者眼里,能够自己去画一个View绝逼是一件很屌很Cool的事!但是,同样而言,自定义View对初学者来说却往往可望而不可及,可望是因为看了很多自定义View的源码好像并不难,有些自定义View甚至不足百行代码,不可及呢是因为即便看了很多文章很多类似的源码依然写不出一个霸气的View来。这时会有很多前辈告诉你多看看View类的源码,看看View类里是如何去处理这些绘制逻辑的,如果你去看了我只能说你是个很好学很有求知欲的孩纸,了解原理是好事,但是并非凡事都要去刨根问底的!如果你做Android开发必须要把Android全部源码弄懂,我只能呵呵了!你还不如去写一个系统实在对吧!同样的道理,写一个自定义View你非要去花巨量时间研究各类源码是不值得提倡的,当然哥没有否定追究原理的意义所在,只是对于一个普通的开发者你没有必要去深究一些不该值得你关心的东西,特别是一个有良好面向对象思维的猿。举个生活中简单的例子,大家都用过吹风,吹风一般都会提供三个档位:关、冷风、热风对吧,你去买吹风人家只会告诉你这吹风三个档位分别是什么功能,我相信没有哪个傻逼买吹风的会把吹风拆开、电机写下来一个一个地跟你解说那是啥玩意吧!同样的,我们自定义View其实Android已经提供了大量类似吹风档位的方法,你只管在里面做你想做的事情就可,至于Android本身内部是如何实现的,你压根不用去管!用官方文档的原话来说就是:Just do you things!初学者不懂如何去自定义View并非是不懂其原理,而是不懂这些类似“档位”的方法! #### 二、实践篇 好了,扯了这么多废话!我们还是先步入正题,来看看究竟自定义View是如何实现的!在Android中自定义一个View类并定是直接继承View类或者View类的子类比如TextView、Button等等,这里呢我们也依葫芦画瓢直接继承View自定义一个View的子类CustomView: ~~~ public class CustomView extends View { } ~~~ 在View类中没有提供无参的构造方法,这时我们的IDE会提示我们你得明确地声明一个和带有父类一样签名列表的构造方法: ![](https://box.kancloud.cn/2016-05-30_574c25e83c9ab.png) 这时我们点击“Add constructor CustomView(Context context)”,IDE就会自动为我们生成一个带有Context类型签名的构造方法: ~~~ public class CustomView extends View { public CustomView(Context context) { super(context); } } ~~~ Context是什么你不用管,只管记住它包含了许多各种不同的信息穿梭于Android中各类组件、控件等等之间,说得不恰当点就是一个装满信息的信使,Android需要它从里面获取需要的信息。 这样我们就定义了一个属于自己的自定义View,我们尝试将它添加到Activity: ~~~ public class MainActivity extends Activity { private LinearLayout llRoot;// 界面的根布局 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); llRoot = (LinearLayout) findViewById(R.id.main_root_ll); llRoot.addView(new CustomView(this)); } } ~~~ 运行后发现什么也没有,空的!因为我们的CustomView本来就什么都没有!但是添加到我们的界面后没有什么问题对吧!Perfect!那我们再直接在xml文档中引用它呢: ~~~ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/main_root_ll" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.sigestudio.customviewdemo.views.CustomView android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> ~~~ 这时我们还原Activity中的代码: ~~~ public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } ~~~ 再次运行后发现IDE报错了: ![](https://box.kancloud.cn/2016-05-30_574c25ece14d5.png) 大致意思是无法解析我们的CustomView类找不到方法,为什么呢?我们在xml文件引用我们的CustomView类时为其指定了两个android自带的两个属性:layout_width和layout_height,当我们需要使用类似的属性(比如更多的什么id啊、padding啊、margin啊之类)时必须在自定义View的构造方法中添加一个AttributeSet类型的签名来解析这些属性: ~~~ public class CustomView extends View { public CustomView(Context context) { super(context); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); } } ~~~ 再次运行发现一切又恢复了正常。现在我们来往我们的View里画点东西,毕竟自定义View总得有点什么才行对吧!Android给我们提供了一个onDraw(Canvas canvas)方法来让我们绘制自己想要的东西: ~~~ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } ~~~ 我们想要画些什么直接在这个方法里面画即可,在现实世界中,我们画画需要两样东西:笔(或者任何能涂画的东西)和纸(或者任何能被画的东西),同样地,Android也给我们提供了这两样东西:Paint和Canvas,一个是画笔而另一个呢当然是画布啦~~,我们可以看到在onDraw方法中,画布Canvas作为签名被传递进来,也就是说这个画布是Android为我们准备好的,不需要你去管,当然你也可以自定义一张画布在上面绘制自己的东西并将其传递给父类,但是一般我们不建议这样去做!有人会问这画布是怎么来的?在这里我不想跟大家深究其原理,否则长篇大论也过于繁琐打击各位菜鸟哥的学习兴趣。但是我可以这样跟大家说,如果在一张大的画布(界面)上面有各种各样小的画布(界面中的各种控件),那么这些小的画布该如何确定其大小呢?自己去想哈哈! 草!又跑题了! 画布有了,差一支画笔,简单!我们new一个呗!程序猿的好处就在万事万物都可以自己new!女朋友也能自己new,随便new!!~~~: ~~~ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); paint.setAntiAlias(true); } ~~~ 实例化了一个Paint对象后我们为其设置了抗锯齿(一种让图像边缘显得更圆滑光泽动感的碉堡算法):setAntiAlias(true),但是我们发现这是IDE又警告了!!!说什么“Avoid object allocations during draw/layout operations (preallocate and reuse instead)”: ![](https://box.kancloud.cn/2016-05-30_574c25ed0cc53.png) Why?Why?说白了就是不建议你在draw或者layout的过程中去实例化对象!为啥?因为draw或layout的过程有可能是一个频繁重复执行的过程,我们知道new是需要分配内存空间的,如果在一个频繁重复的过程中去大量地new对象内存爆不爆我不知道,但是浪费内存那是肯定的!所以Android不建议我们在这两个过程中去实例化对象。既然都这样说了我们就改改呗: ~~~ public class CustomView extends View { private Paint mPaint; public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); // 初始化画笔 initPaint(); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔并打开抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } } ~~~ 现实世界中,我们画画的画笔是多种多样的,有马克笔、铅笔、圆珠笔、毛笔、水彩笔、荧光笔等等等等……而这些笔的属性也各自不同,像铅笔按照炭颗粒的粗糙度可以分为2B、3B、4B、5B、HB当然还有SB,而水彩笔也有各种不同的颜色,马克笔就更霸气了不说了!同样地在Android的画笔里,现实有的它也有,没有的它还有!我们可以用Paint的各种setter方法来设置各种不同的属性,比如setColor()设置画笔颜色,setStrokeWidth()设置描边线条,setStyle()设置画笔的样式: ![](https://box.kancloud.cn/2016-05-30_574c25ed3c1af.png) Paint集成了所有“画”的属性,而Canvas则定义了所有要画的东西,我们可以通过Canvas下的各类drawXXX方法绘制各种不同的东西,比如绘制一个圆drawCircle(),绘制一个圆弧drawArc(),绘制一张位图drawBitmap()等等等: ![](https://box.kancloud.cn/2016-05-30_574c25ed8779e.png) 既然初步了解了Paint和Canvas,我们不妨就尝试在我们的画布上绘制一点东西,比如一个圆环?我们先来设置好画笔的属性: ~~~ /** * 初始化画笔 */ private void initPaint() { // 实例化画笔并打开抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); /* * 设置画笔样式为描边,圆环嘛……当然不能填充不然就么意思了 * * 画笔样式分三种: * 1.Paint.Style.STROKE:描边 * 2.Paint.Style.FILL_AND_STROKE:描边并填充 * 3.Paint.Style.FILL:填充 */ mPaint.setStyle(Paint.Style.STROKE); // 设置画笔颜色为浅灰色 mPaint.setColor(Color.LTGRAY); /* * 设置描边的粗细,单位:像素px * 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素 */ mPaint.setStrokeWidth(10); } ~~~ 然后在我们的onDraw方法中绘制Cricle即可: ~~~ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制圆环 canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, 200, mPaint); } ~~~ 这里要注意哦!drawCircle表示绘制的是圆形,但是在我们的画笔样式设置为描边后其绘制出来的就是一个圆环!其中drawCircle的前两个参数表示圆心的XY坐标,这里我们用到了一个工具类获取屏幕尺寸以便将其圆心设置在屏幕中心位置,第三个参数是圆的半径,第四个参数则为我们的画笔! 这里有一点要注意:在Android中设置数字类型的参数时如果没有特别的说明,参数的单位一般都为px像素。 好了,我们来运行下我们的Demo看看结果: ![](https://box.kancloud.cn/2016-05-30_574c25eddc992.png) 一个灰常漂亮的圆环展现在我们眼前!怎么样是不是很爽,这算是我们写的第一个View,当然这只是第一步,虽然只是一小步,但必定会是影响人类进步的一大步!……Fuck! 不过一个简单地画一个圆恐怕难以满足各位的胃口对吧,那我们尝试让它动起来?比如让它的半径从小到大地不断变化,那怎么实现好呢?大家如果了解动画的原理就会知道,一个动画是由无数张连贯的图片构成的,这些图片之间快速地切换再加上我们眼睛的视觉暂留给我们造成了在“动”的假象。那么原理有了实现就很简单了,我们不断地改变圆环的半径并且重新去画并展示不就成了?同样地,在Android中提供了一个叫invalidate()的方法来让我们重绘我们的View。现在我们重新构造一下我们的代码,添加一个int型的成员变量作为半径值的引用,再提供一个setter方法对外设置半径值,并在设置了该值后调用invalidate()方法重绘View: ~~~ public class CustomView extends View { private Paint mPaint;// 画笔 private Context mContext;// 上下文环境引用 private int radiu;// 圆环半径 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; // 初始化画笔 initPaint(); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔并打开抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); /* * 设置画笔样式为描边,圆环嘛……当然不能填充不然就么意思了 * * 画笔样式分三种: * 1.Paint.Style.STROKE:描边 * 2.Paint.Style.FILL_AND_STROKE:描边并填充 * 3.Paint.Style.FILL:填充 */ mPaint.setStyle(Paint.Style.STROKE); // 设置画笔颜色为浅灰色 mPaint.setColor(Color.LTGRAY); /* * 设置描边的粗细,单位:像素px * 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素 */ mPaint.setStrokeWidth(10); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制圆环 canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, radiu, mPaint); } public synchronized void setRadiu(int radiu) { this.radiu = radiu; // 重绘 invalidate(); } } ~~~ 那么OK,我们在Activity中开一个线程,通过Handler来定时间断地设置半径的值并刷新界面: ~~~ public class MainActivity extends Activity { private CustomView mCustomView;// 我们的自定义View private int radiu;// 半径值 @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { // 设置自定义View的半径值 mCustomView.setRadiu(radiu); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 获取控件 mCustomView = (CustomView) findViewById(R.id.main_cv); /* * 开线程 */ new Thread(new Runnable() { @Override public void run() { /* * 确保线程不断执行不断刷新界面 */ while (true) { try { /* * 如果半径小于200则自加否则大于200后重置半径值以实现往复 */ if (radiu <= 200) { radiu += 10; // 发消息给Handler处理 mHandler.obtainMessage().sendToTarget(); } else { radiu = 0; } // 每执行一次暂停40毫秒 Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } @Override protected void onDestroy() { super.onDestroy(); // 界面销毁后清除Handler的引用 mHandler.removeCallbacksAndMessages(null); } } ~~~ 运行后的效果我就不演示了,项目源码会共享。 但是有一个问题,这么一个类似进度条的效果我还要在Activity中处理一些逻辑多不科学!浪费代码啊!还要Handler来传递信息,Fuck!就不能在自定义View中一次性搞定吗?答案是肯定的,我们修改下CustomView的代码让其实现Runnable接口,这样就爽多了: ~~~ public class CustomView extends View implements Runnable { private Paint mPaint;// 画笔 private Context mContext;// 上下文环境引用 private int radiu;// 圆环半径 public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; // 初始化画笔 initPaint(); } /** * 初始化画笔 */ private void initPaint() { // 实例化画笔并打开抗锯齿 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); /* * 设置画笔样式为描边,圆环嘛……当然不能填充不然就么意思了 * * 画笔样式分三种: * 1.Paint.Style.STROKE:描边 * 2.Paint.Style.FILL_AND_STROKE:描边并填充 * 3.Paint.Style.FILL:填充 */ mPaint.setStyle(Paint.Style.STROKE); // 设置画笔颜色为浅灰色 mPaint.setColor(Color.LTGRAY); /* * 设置描边的粗细,单位:像素px * 注意:当setStrokeWidth(0)的时候描边宽度并不为0而是只占一个像素 */ mPaint.setStrokeWidth(10); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 绘制圆环 canvas.drawCircle(MeasureUtil.getScreenSize((Activity) mContext)[0] / 2, MeasureUtil.getScreenSize((Activity) mContext)[1] / 2, radiu, mPaint); } @Override public void run() { /* * 确保线程不断执行不断刷新界面 */ while (true) { try { /* * 如果半径小于200则自加否则大于200后重置半径值以实现往复 */ if (radiu <= 200) { radiu += 10; // 刷新View invalidate(); } else { radiu = 0; } // 每执行一次暂停40毫秒 Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } } } } ~~~ 而我们的Activity呢也能摆脱繁琐的代码逻辑: ~~~ public class MainActivity extends Activity { private CustomView mCustomView;// 我们的自定义View @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 获取控件 mCustomView = (CustomView) findViewById(R.id.main_cv); /* * 开线程 */ new Thread(mCustomView).start(); } } ~~~ 运行一下看看呗!肏!!!报错了: ![](https://box.kancloud.cn/2016-05-30_574c25edeeebb.png) Why!因为我们在非UI线程中更新了UI!而在Android中非UI线程是不能直接更新UI的,怎么办?用Handler?NO!Android给我们提供了一个更便捷的方法:postInvalidate();用它替代我们原来的invalidate()即可: [java] view plain copy print? @Override public void run() { /* * 确保线程不断执行不断刷新界面 */ while (true) { try { /* * 如果半径小于200则自加否则大于200后重置半径值以实现往复 */ if (radiu <= 200) { radiu += 10; // 刷新View postInvalidate(); } else { radiu = 0; } // 每执行一次暂停40毫秒 Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } } } 运行效果不变。 源码地址:[传送门](http://download.csdn.net/detail/aigestudio/8170091) 温馨提示:自定义控件其实很简单系列文章每周一、周四更新一篇~ 下集精彩预告:Paint为我们提供了大量的setter方法去设置画笔的属性,而Canvas呢也提供了大量的drawXXX方法去告诉我们能画些什么,那么小伙伴们知道这些方法是怎么用的又能带给我们怎样炫酷的效果呢?锁定本台敬请关注:自定义控件其实很简单1/6