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

Posted 启舰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义控件三部曲之绘图篇(十八)——BitmapShader与望远镜效果相关的知识,希望对你有一定的参考价值。

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

系列文章:

android自定义控件三部曲文章索引:http://blog.csdn.net/harvic880925/article/details/50995268

上篇初步给大家展示了封装控件的方法,这篇我们继续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自定义控件三部曲之绘图篇(十三)——Canvas与图层

自定义控件三部曲之绘图篇——Path之贝赛尔曲线和手势轨迹水波纹效果

[转]Android自定义控件三部曲系列完全解析(动画, 绘图, 自定义View)

自定义控件三部曲之绘图篇(十六)——给控件添加阴影效果与发光效果

自定义控件三部曲之绘图篇(二十)——RadialGradient与水波纹按钮效果

自定义控件三部曲之动画篇——联合动画的代码实现