Android自定义控件开发入门与实战(13)Android画布

第十章、Android画布

这一章有点长,可能要分3-5篇blog来讲。

在之前章节提过画布的几个获取方法。除了重写系统的onDraw()、dispatchDraw()方法,还可以通过下面方法获得画布:

  • 通过Bitmap创建。
  • 通过SurfaceView的SurfaceHolder.lockCanvas()函数获取。

另外,我们也可以通过创建Drawable对象,然后将画好的Drawable对象画在画布上,也是创建Bitmap的一种方式。
Drawable类有很多派生类。这些派生类都可以通过Drawable的draw(Canvas canvas)函数将其画到画布上。
由于篇幅有限,所以我们只讲解最常用的ShapeDrawable。

既然提到了ShapeDrawable,就不得不提及shape标签,shape标签可以实现的效果与ShapeDrawable类似,虽不及ShapeDrawable功能强大,但在shape标签的基础上理解ShapeDrawable的用法却非常容易。
注意:shape标签在Java类中对应的是GradientDrawable,而不是ShapeDrawable。有些读者会通过类似ShapeDrawable shapeDrawable=textView.getBackground()的代码来获取shape标签,但其实这样的强转会出错。

神奇的是GraidentDrawable的用法和ShapeDrawable的用法几乎一样,所以学会了一个另一个也学会了。

ShapeDrawable

1、shape标签和GradientDrawable
虽然shape标签对应着GradientDrawable类,但是GradientDrawable也不能shape标签的所有功能,因为GradientDrawable的构造函数如下:

public GradientDrawable()public GradientDrawable(Orientation orientation, @ColorInt int[] colors)

在第二个函数中,GradientDrawable并不能完成shape标签的所能完成的构造矩形、椭圆等功能,而神奇的是,通过ShapeDrawable却可以完成shape标签的所有功能。这边的源码就不再探究了。
大家只需要知道代码中得到shape标签实例的时候要强转GradientDrawable就可以了。

获取shape标签的实例
下面我们就举一个例子来看看如何获取shape标签的实例,以及如何使用它。

下面我们实现这样一个功能:在单击按钮时,给原有的shape标签添加圆角。
首先,新建一个shape文件,放在drawable文件下:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#ff0000"/>
    <stroke android:width="2dp" android:color="#00ff00"
        android:dashGap="5dp" android:dashWidth="5dp"/>
</shape>

接下来放在xml中,给TextView设置该background
最后在java代码中获取实例:

        btnAdd = findViewById(R.id.add_shape_corner);
        tvShape = findViewById(R.id.shape_tv);

        btnAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                GradientDrawable drawable = (GradientDrawable) tvShape.getBackground();
                drawable.setCornerRadius(20);
            }
        });

效果如下:
在这里插入图片描述

2、ShapeDrawable的构造函数
ShapeDrawable的构造函数有两个:

ShapeDrawable()
ShapeDrawable(Shape s)

ShapeDrawable是需要与Shape对象关联起来。所以,如果我们使用第一个构造函数,则需要额外调用ShapeDrawable.setShape(Shape shape)函数来设置Shape对象。

Shape类是一个基类,其中的draw函数是一个虚函数,每个子类可以根据不同需求来会出不同的图形。所以,我们在构造ShapeDrawable时,并不能直接传递shape类型的对象,因为在Shape对象里并没有实现draw函数,而是需要传入已经实现draw函数的Shape类的派生类。Shape的派生类如下:

  • RectShape:构造一个矩形Shape
  • ArcShape:构造一个扇形Shape
  • OvalShape:构造一个椭圆Shape
  • RoundR·ectShape:构造一个圆角矩形Shape,可带有镂空矩形效果。
  • PathShape:构造一个可根据路径绘制的Shape

RectShape
这里我们创建一个RectShape实例:

   public ShapeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setLayerType(LAYER_TYPE_SOFTWARE, null);
        shapeDrawable = new ShapeDrawable(new RectShape());
        shapeDrawable.setBounds(new Rect(50, 50, 200, 100));
        shapeDrawable.getPaint().setColor(Color.RED);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        shapeDrawable.draw(canvas);
    }

(1)在代码中我们创建了一个ShapeDrawable实例,并且在其构造函数中new出RectShape,所以画出来的一定是一个矩形而不是其他形状。
(2)然后我们通过shapeDrawable的setBounds,来指定ShapeDrawable在当前控件中的显示位置(50,50,200,100)。这里的意思是shapeDrawable会在ShapeView中(50,50,200,100)这两个点所定义的矩形区域显示。需要强调的一点是,这里矩形位置是当前控件的位置,而不是全屏幕的位置。
(3)通过shapeDrawable.getPaint().setColor(Color.RED) 来拿到ShapeDrawable自带的paint,然后给该paint设置color为红色。

最后在xml中显示如下:

在这里插入图片描述
然后我们更改其xml代码:

 <com.example.myviewdomo.canvas.ShapeView
      android:layout_width="250px"
      android:layout_height="150px"
      android:layout_margin="100dp"
      android:background="#ffffff" />

这里设置 宽250px,高150px,margin设置100dp,这个shapeDrawable由于宽在50px到200px,高在50-100px,所以在控件中应该是居中显示
在这里插入图片描述
从效果图可以得出下面的结论:

  • ShapeDrawable.setBounds设置的位置的矩形是指在控件中的位置,而不是以左上角为基准的。
  • 通过mShapeDrawable.getPaint()可以直接得到drawable的自带的画笔。

Drawable的画布问题
我们调用shapeDrawable.getPaint()获得画笔填充红色后,那么ShapeDrawable的矩形区域的红色是什么时候被填充上去的呢?
有些人可能为认为是在 shapeDrawable.draw(canvas) 这行代码上画进去的,其它的意思是将ShapeDrawable画到当前控件ShapeView上,并没有绘制ShapeDrawable本身。

那也就只能推测,其实是在getPaint().setColor(Color.RED),执行这句话的时候就把颜色填充上去了,实际上也是这样没错的。
最后我们在ShapeView的onDraw()函数调用shapeDrawable.draw(canvas)将重绘过的ShapeDrawable中的样式已经改变了。

OvalShape
就是画椭圆drawable,我们将上面的代码的 new RectShape()改成 new OvalShape(),别的都不做变动,效果如下:
在这里插入图片描述
???我画了个太阳旗出来???
很明显,setBounds函数指定的矩形而生成了对应的椭圆。

ArcShape
ArcShape是在OvalShape的基础上,通过切割指定的角度而得到对应的扇形图形。
ArcShape只有一个构造函数:

public ArcShape(float startAngle,float sweepAngle)
  • startAngle:开始的角度,0°在椭圆你的X轴的正方向上,即向右向中间。
  • sweepAngle:扫过的角度,,正数顺时针,负数逆时针

我们还是通过改变那行初始化的代码来构造一个扇形:

   ...
   shapeDrawable = new ShapeDrawable(new ArcShape(0, 300));
   ...

效果如下:
在这里插入图片描述
RoundShape
字面上是实现一个圆角矩形,但它不仅可以实现圆角矩形,其本意是实现镂空的圆角矩形。而且内角也可以设置圆角

其构造函数为:

public RoundRectShape(float[] outerRadii, RectF inset , float[] innerRadii)

参数

  • float[] outerRadii:外围矩形的各个角度大小,需要填充8个数字,每两个数字一组,分别对应 左上角、右上角、左下角、右下角 4个角度,每两个一组的数字构成了一个椭圆,第一个数表示x轴半径,第二个轴表示y轴半径。不想指定圆角时,可以填null
  • RectF inset:表示内部矩形与外部矩形的各边边距,4个值表示四条边的距离
  • float[] innerRadii:表示内部矩形的各个角度的大小,同样需要填充8个数字。含义和outerRadii一样。

举一个例子:

        。。。
        float[] outerR = new float[]{12, 12, 12, 12, 0, 0, 0, 0};
        RectF inset = new RectF(6, 12, 15, 3);
        float[] innerR = new float[]{50, 12, 0, 0, 12, 50, 0, 0};
        shapeDrawable = new ShapeDrawable(new RoundRectShape(outerR, inset, innerR));
        。。。

其他的代码都不变,得到的效果如下:
在这里插入图片描述
PathShape
其构造函数如下:

public PathShape(Path path, float stdWidht,float stdHeight)

其中path表示要画的路径。
stdWidth表示标准宽度,即将整个ShapeDrawable的宽度分成多少份,Path中的moveTo(x,y)、line(x2,2)这些函数中的数值在这里其实都是以每一份的位置来计算的。当ShapeDrawable动态变大、变小时,每一份都会变小,而根据这些份的数值画出来的Path图形就会动态缩放。

实现一个实例的代码如下:

     public ShapeView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        setLayerType(LAYER_TYPE_SOFTWARE, null);
        Path path = new Path();
        path.moveTo(0, 0);
        path.lineTo(100, 0);
        path.lineTo(100, 100);
        path.lineTo(0, 100);
        path.close();
        shapeDrawable = new ShapeDrawable(new PathShape(path, 100, 100));
        shapeDrawable.setBounds(new Rect(0, 0, 250, 150));
        shapeDrawable.getPaint().setColor(Color.RED);

    }

为了验证份成n份的概念,这边直接把drawable的大小设置成和控件大小一样了。就跟代码path描述一样,drawable会把控件填满。
效果如下:
在这里插入图片描述
这个时候如果我们把ShapeDrawable的高度份数改成200,那么同样的路径代码:

 shapeDrawable = new ShapeDrawable(new PathShape(path, 100, 200));

高度就变成了原来的一半了。
在这里插入图片描述

自定义Shape
除了系统自带的Shape,我们还可以自定义Shape啦,就是继承Shape类,重写其draw方法。
这个实例就不讲了。以后有兴趣了再来实现。

3、常用函数
(1)、setBounds():指定当前ShapeDrawable在该控件的显示位置
(2)、getPaint():获取ShapeDrawable的paint,然后我们可以实现paint的所有函数
这里有一点,关于Shader,在之前讲shader的时候我们知道Shader是从画布左上角开始绘制的,所以在ShapeDrawable中,给其设置了Shader,也会从ShapeDrawable左上角开始绘制
(3)、其他的函数包括
setAlpha(int alpha)
setColorFilter(ColorFilter colorFilter):设置ColorFilter
setIntrinsicHeight(int height):设置默认高度。当Drawable以setbackGroundDrawable及setImageDrawable方式使用时,会使用默认宽度和默认高度来计算当前Drawable的大小与位置。如果不设置默认的宽高都是-1px。
setIntrinsicWidth(int width):设置默认宽度
setPadding(Rect padding):设置边距

4、放大镜效果
首先看下onDraw函数:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (bitmap == null) {
            Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.bg_telescope);
            bitmap = bmp.createScaledBitmap(bmp, getWidth(), getHeight(), false);
            BitmapShader bmShader = new BitmapShader(Bitmap.createScaledBitmap(bitmap, bitmap.getWidth() * FACTOR, bitmap.getHeight() * FACTOR, true),
                    Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
            drawable = new ShapeDrawable(new OvalShape());
            drawable.getPaint().setShader(bmShader);
            drawable.setBounds(0, 0, RADIUS * 2, RADIUS * 2);
        }
        canvas.drawBitmap(bitmap,0,0,null);
        drawable.draw(canvas);
    }

(1)为什么我们要把bitmap的初始化放在 onDraw方法中,因为我们需要把被放大的图片缩放到控件大小。而控件的大小只有在onLayout之后才知道,所以就在onDraw来拿,至于Bitmap.createScaledBitmap()之后再讲。
(2)创建ShapeDrawable,做的是放大镜形状,所以我们要构造OvalShape,然后大小是事先设定好的radius*2
(3)bmShader就是创建一个将原图放大三倍的图片(因为放大镜放大三倍)

接下来看下onTouchEvent代码:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_DOWN:
                final int x = (int) event.getX();
                final int y = (int) event.getY();
                // 这个位置表示的是绘制Shader 的起始位置
                matrix.setTranslate(RADIUS - x * FACTOR, RADIUS - y * FACTOR);
                drawable.getPaint().getShader().setLocalMatrix(matrix);

                drawable.setBounds(x - RADIUS, y - RADIUS, x + RADIUS, y + RADIUS);
                invalidate();
                return true;
            case MotionEvent.ACTION_UP:
                drawable.setBounds(0,0,0,0);
                invalidate();
                return super.onTouchEvent(event);
            default:
                return super.onTouchEvent(event);
        }
    }

在onTouchEvent中返回了true,表示完全拦截了所有Touch事件,因为这里完全不需要使用View默认的处理行为。
当手指有动作的时候我们应该改变当前ShapeDrawable的位置

     drawable.setBounds(x - RADIUS, y - RADIUS, x + RADIUS, y + RADIUS);

即以当前手指位置为中心,画一个圆。

最关键的是Shader如何移动到我们要显示的位置,我们讲过,Shader的开始显示位置在ShapeDrawable的左上角。所以,如果我们不移动Shape,那么显示出来的永远是图片的左上角部分。

我们需要先找到当前手指放大3倍的图片上对应的点,然后以这个对应点为中心显示出半径为RADIUS的圆中的图形。
对应的点好找,当前手指的位置是(x,y),那么放大的位置是(3x,3y)。为了显示以放大3倍后的手指位置为中心的圆形区域,BitmapShader需要向左和上移动多少呢?
其实画个图就知道 是向左移动 (-3x+r) 向上移动 (-3y+r)

效果如下所示:
在这里插入图片描述

5、自定义Drawable
自定义控件时Android控件的精髓,所以Drawable系统自带的api(ShapeDrawable GradientDrawable)大部分都满足不了我们的需求,所以我们更需要自己学习做,这里来自定义一个Drawable。

让类继承自Drawable,可以看到必须要实现以下抽象函数:

public class CustomDrawable extends Drawable {
    @Override
    public void draw(@NonNull Canvas canvas) {
        
    }

    @Override
    public void setAlpha(int alpha) {

    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {

    }

    @Override
    public int getOpacity() {
        return 0;
    }
}

4个函数意义如下:

  • draw():使我们将会用到的,与View类似,传入的参数是一个Canvas对象,我们只需要调用Canvas的一些方法,效果就会直接显示在Drawable上
  • setAlpha()和setColorFilter()函数非常容易实现。当外层调用Drawable这两个函数时,我们只需要将对应的参数设置给Drawable的Paint即可。
  • getOpacity():当外部需要知道我们自定义的Drawable显示模式时会调用这个函数。它有4个取值:PixelFormat.UNKNOWN,TRANSLUCENT,TRANSPARENT,OPAQUE。其中PexelFormat.UNKNOWN表示当前Drawable的绘图是具有Alpha通道的,即使用Drawable后,其底部的图像仍有可能看得到。PixelFormat.TRANSPARENT表示当前Drawable是完全透明的。PexelFormat.OPAQUE表示当前图像是完全没有Alpha通道的,使用Drawable后,其底层图像将完全被覆盖。PixelFormal.UNKNOWN表示未知。一般而言,如果我们不知道怎么返回,直接返回PixelFormat.TRANSLUCENT是最靠谱的做法。

我们来自定义一个圆角矩形Drawable:

public class CustomDrawable extends Drawable {

    private Paint mPaint;
    private Bitmap mBitmap;
    private BitmapShader bitmapShader;
    private RectF mBounds;

    public CustomDrawable(Bitmap mBitmap) {
        this.mBitmap = mBitmap;
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        canvas.drawRoundRect(mBounds, 20, 20, mPaint);
    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public void setBounds(int left, int top, int right, int bottom) {
        super.setBounds(left, top, right, bottom);

        bitmapShader = new BitmapShader(Bitmap.createScaledBitmap(mBitmap, right - left, bottom - top, true), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mPaint.setShader(bitmapShader);
        mBounds = new RectF(left, top, right, bottom);
    }

    @Override
    public int getIntrinsicWidth() {
        return mBitmap.getWidth();
    }

    @Override
    public int getIntrinsicHeight() {
        return mBitmap.getHeight();
    }
}

我们在getIntrinsicWidth/Height设置了默认的宽高

setBounds()在ShapeDrawable中我们就接触了这个函数,含义是给Drawable设定边界,即这块Drawable画布的大小。在setBounds()函数中,我们根据边界创建一个与Drawable相同大小的Bitmap作为Drawable的Shader。
也就是说Bitmap会根据Drawable的大小动态拉伸,以完全覆盖这个Drawable,最后将边界保存起来,以便在绘图中使用。

draw:我们知道,Sader始终是从画布的左上角开始平铺的,而drawXXX函数只用来指定哪部分显示出来, 所以我们只需要在draw方法中调用drawRoundRect函数就能将BitmapShder以圆角矩形的方式显示出来即可。

Drawable的使用方法
一般有两种使用方法,一种是通过ImageView的setImageDrawable(drawable)函数将其设置为ImageView的源图片,另外一种是通过View的setBackgroundDrawable(drawable)函数将其作为背景

  <ImageView
      android:id="@+id/iv"
      android:layout_width="200dp"
      android:layout_height="200dp"
      android:background="#ffffff"
      android:scaleType="fitXY"/>

然后代码中通过:

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.avator_xizuka);
        iv = findViewById(R.id.iv);
        CustomDrawable drawable = new CustomDrawable(bitmap);
        iv.setImageDrawable(drawable);

在这里插入图片描述
这就跟直接在ImageView中直接设置src一样。

假如我么能通过setBackgroundDrawable来设置,并且在xml文件中将ImageView改成TextView,宽高改成wrap_content,效果如下:
在这里插入图片描述
这个时候图片被拉得很长,宽度 高度是取textView和图片中长的那一个。
这是因为我们设置了默认宽高为bitmap的宽高,如果我们不去设置默认宽高,让其返回-1,效果将是下面这个样子:
在这里插入图片描述
就变成了text的高度和宽度了。

总结:

  • 当使用setImageDrawable(drawable)函数来设置ImageView数据源时,自定义Drawable的位置和大小与ImageView的scaleType有关
  • 当使用setBackgroundDrawable(drawable)函数来设置View的背景时,自定义的Drawable的宽高与控件大小一致,控件的宽、高则选取本身宽高和自定义Drawable的宽高。

自定义Drawable和自定义View差别很大,虽然像是砍掉手势交互的自定义View,但是自定义Drawable的使用场景非常明确,就是使用在有Drawable的地方中。而且也可以替代Bimap用于View中(比如放大镜效果

既然它可以替代Bitmap,那我们就来讲一下它和bitmap之间的关系吧。
6、Drawable与Bitmap的对比

(1)定义对比

  • Bitmap是位图,bmp格式,编码器很多,有RGB_565,ARGB_8888,逐像素的显示对象,执行效率很高,但是存储效率低下
  • Drawable作为Andorid下通用的图形对象,可以装载常用格式的图像,比如GIF PNG JPG BMP,还提供了高级可视化对象,比如渐变、图形

所以Bitmap是Drawable,但是Drawable不一定是Bitmap。
(2)指标对比
在这里插入图片描述
单是从占用内存这一点上,我们就有必要在使用Bitmap一定要考虑可不可以使用Drawable了,绘制速度更是如此。
所以在Android UI系统中普遍使用Drawable。
(3)绘制便利性对比:
Drawable有很多派生类,通过这些派生类可以容易地生成渐变、层叠等效果。但从这一方面而言,Drawable比Bitmap更有优势。如果仅仅用空白画布来绘图,drawable构造和使用则不如bitmap方便。
(4)使用简易性对比
bitmap就是canvas,所以直接用canvas来绘制
而drawable要取画笔绘制,而一般情况下,Drawable子类使用Canvas函数并不方便,所以它只能完成固有的功能。

总结:

  • Bitmap在内存占用和绘制速度上不比Drawable
  • Bitmap绘图方便
  • Drawable的子类可以完成更多的绘图功能。

那么Drawable、Bitmap、自定义View在哪些情况下才会使用呢?
(1)Bitmap只在一种情况下使用,那就是View中需要自己生成图像时,才会使用Bitmap绘图。绘制后的结果保存在这个bitmap中。
(2)当使用Drawable的子类能完成一些固有功能的时候,优先使用Drawable
(3)当需要使用setImageBackground()、setBackgroundDrawable()等可以直接设置Drawable资源的函数时,只能选用Drawable
(4)当在自定义View中在指定位置显示图像功能时,既可以使用Drawable也可以使用Bitmap
(5)除Drawable和Bitmap以外的地方,使用自定义View。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值