怕作者删除,粗粗保存一下,这文章没图,就看原文

---------------------------------------

尊重原创转载请注明: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 {
}
在View类中没有提供无参的构造方法,这时我们的IDE会提示我们你得明确地声明一个和带有父类一样签名列表的构造方法:
这时我们点击“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报错了:

大致意思是无法解析我们的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)”:

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()设置画笔的样式:

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);
}
然后在我们的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看看结果:

一个灰常漂亮的圆环展现在我们眼前!怎么样是不是很爽,这算是我们写的第一个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();
    }
}
运行一下看看呗!肏!!!报错了:

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
————————————————
版权声明:本文为CSDN博主「AigeStudio」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/aigestudio/article/details/41212583

自定义控件 1 (入门)相关推荐

  1. ASP.NET 自定义控件从入门到精通3补充

    ASP.NET 自定义控件从入门到精通 3 状态管理和Style类 3.2 新的Render方法 源码下载 首先我们来看看Register控件在前台生成的Html代码,代码如下所示: <!-注意 ...

  2. Android自定义控件开发入门与实战(1)绘图基础

    今天从leader那里拿到了启舰大神写的<自定义控件开发入门与实战>这本书,据说看完了,至少写起自定义view也不会慌. 最重要的是多练,所以这本书基本设计到的我没有涉及过的控件开发(之前 ...

  3. Android 自定义控件开发入门(一)

    那么怎样来创建一个新的控件呢? 这得看需求是怎样的了. 1.需要在原生控件的基本功能上进行扩展,这个时候你只需要继承并对控件进行扩展.通过重写它的事件,onDraw ,但是始终都保持都父类方法的调用. ...

  4. 自定义控件从入门到轻生之---来个结晶

    注:所有blog局限于博主水平有限,很多不足之处大家可以指出共同探讨进步. 尊重原创转载请注明:From 倪大叶(http://blog.csdn.net/renyi0109) 侵权必究! 一般自定义 ...

  5. 自定义控件从入门到轻生之---初尝禁果

    所有blog局限于博主水平有限,很多不足之处大家可以指出共同探讨进步. 尊重原创转载请注明:From 倪大叶http://blog.csdn.net/renyi0109 侵权必究!虽然我不知道具体怎么 ...

  6. PyQT5学习之旅 1 如何自定义控件,入门做一个上位电脑串口调试软件,全部开源。(附带源码)

    文章目录 一.前言 二.开发的必备工具 2.1 PyCharm 如何集成 QT Designer UI代码转可视化 可视转化UI代码 打包成 exe 软件: 2.2.引进自定义控件 移除此控件为自定义 ...

  7. Android自定义控件开发入门与实战(11)Xfermode,Android程序员如何有效提升学习效率

    mPaint = new Paint(); mPaint.setColor(Color.BLACK); mBitmap = BitmapFactory.decodeResource(getResour ...

  8. Android自定义控件开发入门与实战(7)SVG动画,android底层架构

    move to (50,23) line to(100,25) 而坐标并不是用width和height的坐标,而是viewportWidth和viewportHeight的坐标,(50,23)中50表 ...

  9. 自定义控件从入门到轻生之---解锁新姿势

    所有blog局限于博主水平有限,很多不足之处大家可以指出共同探讨进步. 尊重原创转载请注明:From 倪大叶http://blog.csdn.net/renyi0109 侵权必究!虽然我不知道具体怎么 ...

  10. Android-史上最优雅的实现文件上传、下载及进度的监听,android自定义控件开发入门与实战

    注:如果需要对Http的返回值做解析,可在使用uploadProgress操作符时,传入一个解析器Parser 下载 //文件存储路径 String destPath = getExternalCac ...

最新文章

  1. Go 语言实现字符串匹配算法 -- BF(Brute Force) 和 RK(Rabin Karp)
  2. Java面向对象编程思想
  3. 查看 SQL Server 2000 中数据表所占用的磁盘空间
  4. p,v原语解决和尚挑水问题
  5. 获取某字符 之后 之前
  6. 使用SDL打造游戏世界之入门篇 - 6
  7. 【问链-链改进行时】 第二课 链改的技术架构选择
  8. Codeforces Global Round 15 (A-D)没有C
  9. 在Linux系统安装Nginx及配置https加密访问
  10. Redis配置和常用命令
  11. l2tp连接尝试失败 因为安全层在初始化_不用批归一化也能训练万层ResNet,新型初始化方法Fixup了解一下...
  12. 湘潭大学计算机科学,湘潭大学计算机科学和技术一级学科.doc
  13. NBU3.2及以上版本收集DataCollect和NBSU等日志的统一方法
  14. java 架构师之路
  15. *明确插件的功效*千千静听听音效插件使用介绍及相关下载
  16. 论文翻译(上):Deep Learning Based Semantic Labelling of 3D Point Cloud in Visual SLAM
  17. 到底是什么原因?让200多家企业参与区块链改革?
  18. 1.微信回到首页直接退出网页 2.vue app返回直接退出问题, 首页返回两次退出解决
  19. 致那些喜欢站在上帝视角的人
  20. Numpy库的下载及安装(吐血总结)

热门文章

  1. CSS3 Animation 帧动画 steps() --冯浩的博客
  2. 西安IATF16949认证_西安IATF16949咨询_8.3.3.2制造过程设计输入
  3. 中国高速公路企业破局之道 ——三管齐下:流量变现、新觅赛道、精益运营
  4. 9 二叉树的重建--来源于沈钰S同学(舒姐)
  5. 蓝凌夺得2017移动办公年度产品和厂商双项大奖
  6. c++学习笔记day03
  7. HTML——表格标签
  8. 高数 08.06 微分方程习题课 01
  9. VB连接数据库时出现“ActiveX部件不能创建对象”
  10. [乐意黎转载]高效 jquery 的奥秘