IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [原]自定义控件三部曲之绘图篇(十八)——BitmapShader与望远镜效果

    harvic880925发表于 2016-07-26 19:59:19
    love 0

    前言:不逼自己一把,你永远不知道自己有多优秀。

    上篇初步给大家展示了封装控件的方法,这篇我们继续Paint来看相关方法的用法,这篇我们将会讲一个很起来没啥用,但效果却很屌的方法setShader,这篇文章最后,我们将实现的效果是望远镜效果:(看起来有没有屌屌的)
    这里写图片描述
    我们先来看看setShader函数的声明:

    //Paint类中的方法
    public Shader setShader(Shader shader)

    Shader在三维软件中称之为着色器,就是用来给空白图形上色用的。在PhotoShop中有一个工具叫印章工具,我们能够指定印章的样式来填充图形。印章的样式可以是图像、颜色、渐变色等。这里的Shader实现的效果与印章类似。我们也是通过给Shader指定对应的图像、渐变色等来填充图形的。
    Shader类只是一个基类,它其中只有两个方法setLocalMatrix(Matrix localM)、getLocalMatrix(Matrix localM)用来设置坐标变换矩阵的,有关设置矩阵的内容,我们后面会单独讲解坐标矩阵用法的时候,会再次提,这里就先略过。
    Shader类与ColorFiler一样,其实是一个空类,它的功能的实现,主要是靠它的派生类来实现的。继承关系如下:
    这里写图片描述
    下面我们就来逐个来看每个派生类的用法与效果。

    一、BitmapShader基本用法

    1、概述

    我们这篇文章只看一个派生类:BitmapShader,它的构造函数如下:

    public BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)

    这个就相当于PhotoShop中的图案印章工具,bitmap用来指定图案,tileX用来指定当X轴超出单个图片大小时时所使用的重复策略,同样tileY用于指定当Y轴超出单个图片大小时时所使用的重复策略
    其中TileMode的取值有:

    • TileMode.CLAMP:用边缘色彩填充多余空间
    • TileMode.REPEAT:重复原图像来填充多余空间
    • TileMode.MIRROR:重复使用镜像模式的图像来填充多余空间

    只看这些还是啥都不懂,我们先来举个例子来看下用法

    2、BitmapShader使用示例

    这里使用的印章图像是:(dog_edge.png)
    这里写图片描述
    中间是我们熟悉的小狗,四周被四种不同的颜色给包围,这些颜色是我特地画上去的,后面自然有它的用处。
    我们还是先直接来看完整代码吧:

    public class BitmapShaderView extends View {
        private Paint mPaint;
        private Bitmap mBmp;
        public BitmapShaderView(Context context) {
            super(context);
            init();
        }
    
        public BitmapShaderView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public BitmapShaderView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init();
        }
    
        private void init(){
            mPaint = new Paint();
            mBmp = BitmapFactory.decodeResource(getResources(),R.drawable.dog_edge);
            mPaint.setShader(new BitmapShader(mBmp, TileMode.REPEAT, TileMode.REPEAT));
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //getWidth()用于获取控件宽度,getHeight()用于获取控件高度
            canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
        }
    }

    代码其实很简单,在初始化的时候设置印章图片:

    private void init(){
        mPaint = new Paint();
        mBmp = BitmapFactory.decodeResource(getResources(),R.drawable.dog_edge);
        mPaint.setShader(new BitmapShader(mBmp, TileMode.REPEAT, TileMode.REPEAT));
    }

    然后在绘图的时候,利用paint绘制一个矩形,这个矩形的大小与控件的大小一模一样:

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //getWidth()用于获取控件宽度,getHeight()用于获取控件高度
        canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
    }

    然后在布局中使用时:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:orientation="vertical"
                  android:layout_width="fill_parent"
                  android:layout_height="fill_parent">
        <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="test BTN"/>
    
        <com.harvic.Blog_BitmapShader.BitmapShaderView
                android:layout_width="200dp"
                android:layout_height="400dp"
                android:layout_gravity="center_horizontal"/>
    </LinearLayout>

    给我们自定义的控件添加上宽高限制,为了方便看效果,我在它上面也另外加了一个按钮
    效果图如下:
    这里写图片描述
    从效果图中可以看出:

    • 使用X轴和Y轴都使用REPEAT模式下,在超出单个图像的区域后,就会重复绘制这个图像
    • 绘制是从控件的左上角开始的,而不是从屏幕原点开始的!这点很好理解,因为我们绘图也只会在自定义控件上绘图,不会在全屏幕上绘图。

    3、TileMode模式解析

    上面初步看到了REPEAT模式的用法,现在我们分别来看在各个模式下的不同表现
    (1)、TileMode.REPEAT模式:重复原图像来填充多余空间
    在更改模式时,只需要更新setShader里的代码:

    mPaint.setShader(new BitmapShader(mBmp, TileMode.REPEAT, TileMode.REPEAT));

    在这里,X轴、Y轴全部设置成REPEAT模式,所以当控件的显示范围超出了单个图的显示范围时,在X轴上将使用REPEAT模式,同样,在Y轴上也将使用REPEAT模式
    效果图如下:
    这里写图片描述
    (2)、TileMode.MIRROR模式:重复使用镜像模式的图像来填充多余空间
    同样,将X轴、Y轴全部改为MIRROR模式:

    mPaint.setShader(new BitmapShader(mBmp, TileMode.MIRROR, TileMode.MIRROR));

    效果图如下:
    这里写图片描述
    先看效果图的X轴:在X轴上每两张图片的显示都像镜子一样翻转一下。
    同样,在Y轴上每两张图片的显示也都像镜子一样翻转一下。
    所以这就是镜相效果的作用,镜相效果其实就是在显示下一图片的时候,就相当于两张图片中间放了一个镜子一样。
    (3)、TileMode.CLAMP:用边缘色彩填充多余空间
    同样,我们还是将X轴、Y轴全部改为CLAMP模式:

    mPaint.setShader(new BitmapShader(mBmp, TileMode.CLAMP, TileMode.CLAMP));

    效果图如下:
    这里写图片描述
    CLAMP模式的意思就是当控件区域超过当前单个图片的大小时,空白位置的颜色填充就用图片的边缘颜色来填充。
    (4)、TileMode.CLAMP与填充顺序
    我们还是先来看一下原图像:
    这里写图片描述
    按照我们上面讲的,当X轴、Y轴全部都是CLAMP模式时,X轴的空白区域会用图像的右侧边缘颜色来填充;Y轴的空白区域会用图像的底部的边缘颜色来填充,那效果应该是这样的:
    这里写图片描述
    明显右下角的空白位置根本与图像是不沾边的,那它要用什么颜色来填充呢?是填充上方的蓝色还是填充左侧的绿色呢?
    从最终的效果图来看,这部分填充的颜色是绿色的,可为什么呢?
    其实这是跟填充顺序有关的,因为我们同时要填充横向和竖向;那到底是先填充横向还是先填充竖向呢?
    答案是先填充竖向!在填充竖向后的结果如下:
    这里写图片描述
    在填充竖向后,整个竖向都是有颜色的了,此时再根据竖向的边缘色彩来填充横向:
    这里写图片描述
    红色方框的区域就是根据竖向的边缘色彩来填充的,这样,当X轴Y轴全是CLAMP时,就理解为什么右下角是填充的绿色而不是蓝色的原因了。
    (5)、当MIRROR与REPEAT混用时
    TileMode.MIRROR, TileMode.REPEAT
    上面我们在填充X轴 和Y轴的空白位置时,都是用的同一种模式,下面我们就来看一下当X轴与Y轴的填充模式不一样时,效果又是怎样的呢?
    这里我们假设X轴填充空白区域时,使用MIRROR样式、在填充Y轴空白区域时,使用REPEAT样式:

    mPaint.setShader(new BitmapShader(mBmp, TileMode.MIRROR, TileMode.REPEAT));

    效果图如下:
    这里写图片描述
    无论哪两种模式混合,我们在理解时只需要记着填充顺序是先填充Y轴,然后再填充X轴!这样效果图就很好理解了
    首先,是先填充Y轴,在填充Y轴时使用的是REPEAT模式,此时的效果图是:
    这里写图片描述
    在填充Y轴以后再利用X轴的镜相模式来填充X轴,这样整个控件就被填充完毕了。
    TileMode.REPEAT,TileMode.MIRROR,
    下面我们再反过来看一下当X轴使用REPEAT模式,Y轴使用MIRROR模式效果会怎样:

    mPaint.setShader(new BitmapShader(mBmp, TileMode.REPEAT, TileMode.MIRROR));

    效果图如下:
    这里写图片描述
    同样是先使用镜相模式来填充Y轴,然后再使用REPEAT模式来填充X轴;所以从效果图中可以明显看出第一列的Y轴全部是镜相效果。然后再根据第一列的镜相效果来填充X轴,由于X轴使用的是REPEAT模式,所以X轴的图像全部都与左侧第一列的图像相同。
    (6)、CLAMP模式与其它模式混用
    上面我们理解了填充顺序的意义以后,下面再来看一下最难的两种混用方式,就是当CLAMP模式与其它模式混用时的效果。
    比如,当X轴使用CLAMP效果填充,而Y轴使用MIRROR效果填充时:

    mPaint.setShader(new BitmapShader(mBmp, TileMode.CLAMP, TileMode.MIRROR));

    效果图如下:
    这里写图片描述
    从效果图中很好理解,先填充Y轴,填充以后的Y轴各个图像是镜相分布的。而此时再使用CLAMP模式来填充X轴,会拿Y轴图像最边缘的颜色来进行填充。理解难度不大,就不再细讲了。
    下面再将这两种模式反过来,X轴使用MIRROR模式而Y轴使用CLAMP模式:

    mPaint.setShader(new BitmapShader(mBmp, TileMode.MIRROR, TileMode.CLAMP));

    效果图如下:
    这里写图片描述
    想必大家看到效果图以后,也理解为什么会出现这种效果了,这里就不再讲了,如果还不懂,把上面讲的再看一遍。

    4、绘图位置与模式的关系

    在上面的例子中,我们利用drawRect把整个控件大小都给覆盖了,那假如我们只画一个小矩形而不完全覆盖整个控件,那我们SetShader的图片是从哪里开始画的呢?
    是从开始drawRect所绘矩形的左上角开始画,还是在控件的左上角开始的呢?
    我们举个例子来看下:

    public class BitmapShaderView extends View {
        private Paint mPaint;
        private Bitmap mBmp;
        public BitmapShaderView(Context context) {
            super(context);
            init();
        }
    
        public BitmapShaderView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public BitmapShaderView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init();
        }
    
        private void init(){
            mPaint = new Paint();
            mBmp = BitmapFactory.decodeResource(getResources(),R.drawable.dog_edge);
            mPaint.setShader(new BitmapShader(mBmp, TileMode.MIRROR, TileMode.CLAMP));
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.drawRect(100,20,200,200,mPaint);
        }
    }

    上面的代码并没有改变什么,我们需要注意的只有两点:
    第一:设置的重复模式:

    mPaint.setShader(new BitmapShader(mBmp, TileMode.MIRROR, TileMode.CLAMP));

    第二:绘图时,仅绘制一小块矩形:

    canvas.drawRect(100,20,200,200,mPaint);

    效果图如下:
    这里写图片描述
    这是个什么鬼……根本看不懂啊有木有……我们再回来看一下当所绘矩形覆盖整个控件时的效果图:
    这里写图片描述
    而我们这里的效果图根本就是这个完整的图片上扣出来的一小块有没有……

    其实这正说明了一个问题:无论你利用绘图函数绘多大一块,在哪绘制,与Shader无关。因为Shader总是在控件的左上角开始,而你绘制的部分只是显示出来的部分而已。没有绘制的部分虽然已经生成,但只是不会显示出来罢了。

    利用这个特性,我们就可绘制我们的最上面的望远镜效果了

    5、望远镜效果

    我们只需要按照控件大小平铺当前所要绘制的图形的Shader,然后再画出来一个圆圈来当做望远镜就可以了。
    我们先用一张做为Shader的背景图:
    这里写图片描述
    在看完所使用的背景以后,我们再来看下效果图
    这里写图片描述
    这里有两个功能:
    首先,将图片拉伸来覆盖整个控件;
    然后,首先给控件设置进BitmapShader,然后在手指的位置画一个半径为150的圆就可以了。
    正是由于在Paint设置了Shader以后,无论我们绘图位置在哪,Shader中的图片都是从控件的左上角开始填充的,而我们所使用的绘图函数只是用来指定哪部分显示出来,所以当我们在手指按下位置画上一个圆形时,就会把圆形部分的图像显示出来了,看起来就是个望远镜效果。
    然后完整代码如下:

    public class TelescopeView extends View {
        private Paint mPaint;
        private Bitmap mBitmap,mBitmapBG;
        private int mDx = -1, mDy = -1;
        public TelescopeView(Context context) {
            super(context);
            init();
        }
    
        public TelescopeView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public TelescopeView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            init();
        }
    
        private void init() {
            mPaint = new Paint();
            mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scenery);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mDx = (int) event.getX();
                    mDy = (int) event.getY();
                    postInvalidate();
                    return true;
                case MotionEvent.ACTION_MOVE:
                    mDx = (int) event.getX();
                    mDy = (int) event.getY();
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mDx = -1;
                    mDy = -1;
                    break;
            }
    
    
            postInvalidate();
            return super.onTouchEvent(event);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (mBitmapBG == null){
                mBitmapBG = Bitmap.createBitmap(getWidth(),getHeight(), Bitmap.Config.ARGB_8888);
                Canvas canvasbg = new Canvas(mBitmapBG);
                canvasbg.drawBitmap(mBitmap,null,new Rect(0,0,getWidth(),getHeight()),mPaint);
            }
    
            if (mDx != -1 && mDy != -1) {
                mPaint.setShader(new BitmapShader(mBitmapBG, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
                canvas.drawCircle(mDx, mDy, 150, mPaint);
            }
        }
    }

    我们主要来看下OnDraw函数:
    在onDraw函数中,第一部分,就是新建一个空白的bitmap,这个bitmap的大小与控件一样,然后把我们的背景图进行拉伸,画到这个空白的bitmap上。

    if (mBitmapBG == null){
       mBitmapBG = Bitmap.createBitmap(getWidth(),getHeight(), Bitmap.Config.ARGB_8888);
       Canvas canvasbg = new Canvas(mBitmapBG);
       canvasbg.drawBitmap(mBitmap,null,new Rect(0,0,getWidth(),getHeight()),mPaint);
    }

    由于这里的canvasbg是用mBitmapBG创建的,所以所画的任何图像都会直接显示在mBitmapBG上,而我们创建的mBitmapBG是与控件一样大的,所以当把mBitmapBG做为Shader来设置给paint时,mBitmapBG会正好覆盖整个控件,而不会有多余的空白像素。
    这里需要注意的就是我们在将原图像画到mBitmapBG时,进行了拉压缩,把它拉伸到根当前控件一样大小。
    然后利用Shader的知识,利用OnMotionEvent来捕捉用户的手指位置,当用户手指下按时,在手指位置画一个半径为150的圆形,把对应的位置的图像显示出来就可以了:

    if (mDx != -1 && mDy != -1) {
        mPaint.setShader(new BitmapShader(mBitmapBG, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
        canvas.drawCircle(mDx, mDy, 150, mPaint);
    }

    这个控件的难度并不大,问题就在于理解Shader中图像的起始布局位置和显示图像的关系。

    二、利用BitmapShader生成不规则头像

    这部分,我们还得利用Shader的从控件左上角开始布局的原理和显示图像的关系,来讲解一个我们平时经常用到的控件:不规则头像,效果图如下:
    这里写图片描述

    前面我们已经教大家了一种生成不规则头像的方法,大家还记得不,使用xfermode!不记得的同学再翻翻这里:《自定义控件三部曲之绘图篇(十一)——Paint之setXfermode(二)》
    这篇我们就来讲解另一种生成不规则头像的方法,大家赶紧喜大普奔吧

    这里我们依然教大家如何将它封装成一个控件,这里所实现的效果有:圆形图像,方形带圆角的头像。

    1、初步实现圆形头像控件

    这部分,我们先讲原理,初步实现下面的控件效果
    这里写图片描述
    原始的头像是这样的:
    这里写图片描述
    很明显我们给头像加了个圆框效果;
    我们直接来看代码吧:

    public class AvatorViewDemo extends View {
        private Paint mPaint;
        private Bitmap mBitmap;
        private BitmapShader mBitmapShader;
    
        public AvatorViewDemo(Context context, AttributeSet attrs) throws Exception{
            super(context, attrs);
            init();
        }
    
        public AvatorViewDemo(Context context, AttributeSet attrs, int defStyle) throws Exception{
            super(context, attrs, defStyle);
            init();
        }
    
        private void init() throws Exception{
            mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.avator);
    
            mPaint = new Paint();
            mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            Matrix matrix = new Matrix();
            float scale = (float) getWidth()/mBitmap.getWidth();
            matrix.setScale(scale,scale);
            mBitmapShader.setLocalMatrix(matrix);
            mPaint.setShader(mBitmapShader);
    
            float half = getWidth()/2;
            canvas.drawCircle(half,half,getWidth()/2,mPaint);
        }
    }

    首先是初始化:

    private void init() throws Exception{
        mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.avator);
    
        mPaint = new Paint();
        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    }

    在初始化时创建一个BitmapShader,填充模式分别是TileMode.CLAMP、TileMode.CLAMP,其实这里填充模式没什么用,因为我们只需要显示当前图片;所以不存在多余空白区域,所以使用哪种填充模式都无所谓。
    最关键的部分在于绘图:

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Matrix matrix = new Matrix();
        float scale = (float) getWidth()/mBitmap.getWidth();
        matrix.setScale(scale,scale);
        mBitmapShader.setLocalMatrix(matrix);
        mPaint.setShader(mBitmapShader);
    
        float half = getWidth()/2;
        canvas.drawCircle(half,half,getWidth()/2,mPaint);
    }

    绘图时,首先是将BitmapShader缩放到与控件宽高一致;由于我们要画的是一个圆形图像,所以我们必须将图像缩放成一个正方形,而边长就与控件的宽度一致即可:

    Matrix matrix = new Matrix();
    float scale = (float) getWidth()/mBitmap.getWidth();
    matrix.setScale(scale,scale);
    mBitmapShader.setLocalMatrix(matrix);
    mPaint.setShader(mBitmapShader);

    这块就使用了位置矩阵的知识,同时也用上了我们开篇时所讲的mBitmapShader.setLocalMatrix(matrix)函数,有关位置矩阵我们会在后面的章节中涉及,这里只需要知道这块是用来缩放BitmapShader的即可。
    然后利用BitmapShader是从控件左上角开始平铺的,利用canvas.drawXXX系列函数只是用来定义显示哪一块的原理,我们在图片的正中心画一个圆,半径是图片正方形半径的一半:

    float half = getWidth()/2;
    canvas.drawCircle(half,half,getWidth()/2,mPaint);

    在理解了上面的原理之后,再理解这个就难度不大了。
    我们这里只画了个圆形,如果我们画一个矩形,那显示的控件就是个矩形了,如果我们画一个五角形,那头像也就是个五角形效果了;
    原理到这里就讲完了,下面我们先来看几个开源例子,然后再继续讲解如何将其封装成控件的知识。
    下面我们来看一下几个开源的例子:
    地址:https://github.com/hdodenhof/CircleImageView
    效果图
    这里写图片描述
    这个效果大家现在会做了没,它不仅实现了圆形头像效果,还而还在外边加了一个带颜色的边。
    想必在头像外围加个描边不是什么给事吧……悄悄提醒一下,只需要在画圆时,将paint设置为mPaint.setStyle(Paint.Style.STROKE);即可画出来描边。大家自己试试怎么来实现这个控件吧。开源地址:
    第二个开源例子:
    地址:https://github.com/MostafaGazar/CustomShapeImageView
    效果图为:
    这里写图片描述
    具体这些效果在这个控件中是怎么实现的,我也没有看。但敢肯定的是,通过BitmapShape是肯定可以实现的,只要我们利用Path可以做出各种图形(比如这里的五角形,这里的心形),然后利用canvas在Shape上显示出来这块区域就可以了。但明显难度是比较大的,所以对于这类复杂的图像显示,还是建议使用以前我们讲过的Xfermode来做图像混合即可。
    好了,原理就讲到这了,下面我们把上面的圆形控件加以封装,做出来一个通用控件

    2、封装成控件

    上篇文章已经教大家如何封装成控件了,这部分我们再来看这个例子是如何封装成控件的。
    我们再重新来看一下效果图,看我们这部分要做哪些效果出来
    这里写图片描述
    从效果图来看,我们要做两个效果:

    • 圆形图像
    • 矩形图像,但带自定义大小的圆角

    (1)、自定义属性
    封装控件最难免的就是自定义属性了,这块是必会的。
    根据我们需要做的两个效果,所以我们自定义属性时,首先要有一个属性来标识当前是做圆形头像还是做矩形图像
    然后如果是矩形图像,还要有一个值来表示矩形图像的圆角有多大
    最后,还需要一个src的属性来让用户引入Bitmap图像
    所以,此时的定义属性应该是:(values/attrs.xml)

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="AvatorView">
            <attr name="src" format="reference" />
            <attr name="format" format="enum">
                <enum name="circle" value="0"/>
                <enum name="rectTangle" value="1"/>
            </attr>
            <attr name="radius" format="integer" />
        </declare-styleable>
    </resources>

    这里定义了三个属性:

    • src:对应BitmapShader所需要使用的图片
    • format:是一个枚举值,用于指定当前是做圆形图像还是做矩形图像
    • radius:是矩形图像时的边角圆形半径

    (2)、控件解析
    在自定义的属性之后,我们就开始我们自定义控件的JAVA代码征程了,首先是解析自定义属性:

    public class AvatorView extends View {
        private Paint mPaint;
        private Bitmap mBitmap;
        private BitmapShader mBitmapShader;
        private int mEnumFormat =0,mRadius = 5;
    
        public AvatorView(Context context, AttributeSet attrs) throws Exception{
            super(context, attrs);
            init(context,attrs);
        }
    
        public AvatorView(Context context, AttributeSet attrs, int defStyle) throws Exception{
            super(context, attrs, defStyle);
            init(context,attrs);
        }
    
        private void init(Context context,AttributeSet attrs) throws Exception{
            /**
             * 提取属性定义
             */
            TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.AvatorView);
            int BitmapID = typedArray.getResourceId(R.styleable.AvatorView_src,-1);
            if (BitmapID == -1){
                throw new Exception("AvatorView 需要定义Src属性,而且必须是图像");
            }
            mBitmap = BitmapFactory.decodeResource(getResources(),BitmapID);
            mEnumFormat = typedArray.getInt(R.styleable.AvatorView_format,0);
            if (mEnumFormat == 1){
                mRadius = typedArray.getInt(R.styleable.AvatorView_radius,5);
            }
    
            typedArray.recycle();
    
            mPaint = new Paint();
            mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        }
        …………
    }    

    这部分与上篇相同,就不再讲了,无外乎就是在初始化的时候解析控件自定义属性,没什么难度。
    (3)、自测量
    然后我们需要在用户定义layout_width、layout_height的值为wrap_content时,自测量控件大小
    即重写onMeasuer()

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    
        int width = mBitmap.getWidth();
        int height = mBitmap.getHeight();
        setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
    }

    主要就是当用户将控件的layout_with或者layout_heigt设置成wrap_content时,将宽高设置成图片的宽高。这块我们在上篇也已经讲过了,这里也就不再细讲了。不理解的同学自行翻看上一篇。
    (4)、绘图
    最后一部分,到了正式绘图了:

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Matrix matrix = new Matrix();
        float scale = (float) getWidth()/mBitmap.getWidth();
        matrix.setScale(scale,scale);
        mBitmapShader.setLocalMatrix(matrix);
        mPaint.setShader(mBitmapShader);
    
        float half = getWidth()/2;
    
        if (mEnumFormat == 0){
            canvas.drawCircle(half,half,getWidth()/2,mPaint);
        }else  if(mEnumFormat == 1){
            canvas.drawRoundRect(new RectF(0,0,getWidth(),getHeight()),mRadius,mRadius,mPaint);
        }
    }

    也没啥难度,就是先放大BitmapShader到与控件相同大小,然后根据用户要画的是圆形还是圆角矩形,调用不同的绘图函数即可。
    (5)、XML中使用
    然后我们来看如何在布局中使用这个控件(main.xml):

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:app="http://schemas.android.com/apk/res/com.harvic.Blog_BitmapShader"
                  android:orientation="vertical"
                  android:layout_width="fill_parent"
                  android:layout_height="fill_parent">
    
        <com.harvic.Blog_BitmapShader.AvatorView
            android:layout_width="50dp"
            android:layout_height="50dp"
            app:src="@drawable/avator"
            app:format="circle"/>
    
        <com.harvic.Blog_BitmapShader.AvatorView
                android:layout_width="100dp"
                android:layout_height="100dp"
                app:src="@drawable/avator"
                app:format="circle"/>
    
        <com.harvic.Blog_BitmapShader.AvatorView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:layout_marginTop="10dp"
                app:src="@drawable/avator"
                app:format="rectTangle"
                app:radius="10"/>
    
    
        <com.harvic.Blog_BitmapShader.AvatorView
                android:layout_width="130dp"
                android:layout_height="130dp"
                android:layout_marginTop="10dp"
                app:src="@drawable/avator"
                app:format="rectTangle"
                app:radius="30"/>
    
        <com.harvic.Blog_BitmapShader.AvatorView
                android:layout_width="150dp"
                android:layout_height="150dp"
                android:layout_marginTop="10dp"
                app:src="@drawable/avator"
                app:format="rectTangle"
                app:radius="70"/>
    </LinearLayout>

    效果图就是如开篇所示,从布局代码中可以看到,我们已经实现了这个自定义控件,我们不仅可以指定大小,而且还可以使用wrap_content来自适应大小。我这里没有使用wrap_content的主要原因在于图片太大,使用wrap_content的话就画不了几张图了,大家底下自已试试吧
    (6)、存在问题
    有些同学每次都是拿博主控件直接用到项目中,这可是不行的哦,因为博主在这里讲的自定义控件都是抛砖引玉的,主要核心在原理讲解,并没有做太多的容错的……
    这个控件也不例外,我们这里强制使用的正方的图像,当图像是矩形时,就会出现问题,大家需要自己做容错处理,判断图像的宽高,缩放图像以至在不改变图片比例的情况下,填充控件。
    这篇文章就到这了,下篇继续来看Shader的其它派生类的用法和实例。

    源码在文章底部给出

    如果本文有帮到你,记得加关注哦
    源码下载地址:http://download.csdn.net/detail/harvic880925/9586905
    转载请标明出处,http://blog.csdn.net/harvic880925/article/details/52039081谢谢

    如果你喜欢我的文章,那么你将会更喜欢我的微信公众号,将定期推送博主最新文章与收集干货分享给大家(一周一次)
    这里写图片描述



沪ICP备19023445号-2号
友情链接