尊重原创转载请注明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵权必究!
炮兵镇楼
自定义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 {
- }
这时我们点击“Add constructor CustomView(Context context)”,IDE就会自动为我们生成一个带有Context类型签名的构造方法:
- public class CustomView extends View {
- public CustomView(Context context) {
- super(context);
- }
- }
这样我们就定义了一个属于自己的自定义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));
- }
- }
- <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>
- public class MainActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
- }
大致意思是无法解析我们的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);
- }
- }
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- }
草!又跑题了!
画布有了,差一支画笔,简单!我们new一个呗!程序猿的好处就在万事万物都可以自己new!女朋友也能自己new,随便new!!~~~:
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- Paint paint = new Paint();
- paint.setAntiAlias(true);
- }
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);
- }
- }
Paint集成了所有“画”的属性,而Canvas则定义了所有要画的东西,我们可以通过Canvas下的各类drawXXX方法绘制各种不同的东西,比如绘制一个圆drawCircle(),绘制一个圆弧drawArc(),绘制一张位图drawBitmap()等等等:
既然初步了解了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);
- }
- @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);
- }
这里有一点要注意:在Android中设置数字类型的参数时如果没有特别的说明,参数的单位一般都为px像素。
好了,我们来运行下我们的Demo看看结果:
一个灰常漂亮的圆环展现在我们眼前!怎么样是不是很爽,这算是我们写的第一个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();
- }
- }
- 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();
- }
- }
- }
- }
- 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();
- }
- }
Why!因为我们在非UI线程中更新了UI!而在Android中非UI线程是不能直接更新UI的,怎么办?用Handler?NO!Android给我们提供了一个更便捷的方法:postInvalidate();用它替代我们原来的invalidate()即可:
- @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();
- }
- }
- }
源码地址:传送门
温馨提示:自定义控件其实很简单系列文章每周一、周四更新一篇~
下集精彩预告:Paint为我们提供了大量的setter方法去设置画笔的属性,而Canvas呢也提供了大量的drawXXX方法去告诉我们能画些什么,那么小伙伴们知道这些方法是怎么用的又能带给我们怎样炫酷的效果呢?锁定本台敬请关注:自定义控件其实很简单1/6