Android 图像绘制之 Drawable


Android 图像绘制系列文章
Android图像绘制之Bitmap
Android 图像绘制之 Drawable
Android 图像绘制之 Drawable(二)


不要冲动去自定义 View

记得当初学习 Android 的时候,接触到自定义 View 的时候,感觉逼格好高。后来公司要开发一个锁屏界面 ,科长让我接下这个任务带领同事开发,吓得我当时就接受了这个任务。然后在跌跌撞撞中完成了,效果我还算满意。但是后来我仔细回想下开发过程,很多问题我都给自己一个问号。那么今天我提一个问题,为什么要自定义 View?

举个常见例子,我想显示一张圆形图片。说实话,当初的我第一想法就是继承 View,重写 onDraw() 方法。后来回想下,MDZZ,Android 能不能智能点,这也需要我自定义 View,万一再要显示一张圆形纯背景色,我岂不又要自定义 View。 其实我可以告诉你,绘制简单的图形,以及显示静态的图片,可以用 Drawable,而不用自定义 View。例如显示一张圆形图片,我其实可以用 RoundBitmapDrawable,这个后面我们会看到。

Drawable是什么

查了下官方文档,Drawable 是一个抽象的概念,“something that can be drawn”,就我的理解话,Drawable 是一种媒介,它可以把内容绘制到 Canvas 上。

如果你还没理解,举个例子,BitmapDrawable 包装了一个 Bitmap,如果你还不了解 Bitmap,请看我的文章 Android 图像绘制之 Bitmap。其实 BitmapDrawable 最终也是调用了 Canvas 的 drawBitmap() 方法来把 Bitmap 绘制图像到 View 中的。

Drawable的实现类

查了下 Drawable 的实现类,如ShapeDrawable,BitmapDrawable,ClipDrawable,RoundBitmapDrawable,StateListDrawableRoateDrawable。从类的名子来看,大致可以猜到这些类是干嘛的吧,是不是有种相见恨晚的感觉,那么本篇文章就是介绍这些类。当然网上很多文章都有写过,本篇文章不注重抠每个细节,但是会让你感受到这个类到底实际中有何用。

GradientDrawable

一个创建图形的 Drawable,图形包括直线(line), 椭圆(oval),圆形(oval,当椭圆的宽高相等的时候就是圆形),矩形(rectangle),圆环(ring)。不过在这之前,我需要你对 Paint 有所了解,目前我还没有写关于 Paint 的文章,我希望你能从其他文章了解下,这样能更好理解 GradientDrawable 。

GradientDrawable 它在 XML 文件中对应的根节点为 <Shape>,但是它生成的不是 ShapeDrawable,而是 GradientDrawable,但是又达到了 ShapeDrawable 的效果, 我查了下官方文档,没有错,这 TM 就有点搞事了。如果有人知道,可以告诉我为什么。

直线

<?xml version="1.0" encoding="utf-8"?>
<shape    
    xmlns:android="http://schemas.android.com/apk/res/android"    
    android:shape="line">    
    <stroke android:width="5dp" android:color="#ffff0000"/>
</shape>
line.PNG

椭圆或者圆形

<?xml version="1.0" encoding="utf-8"?>
<shape    xmlns:android="http://schemas.android.com/apk/res/android"    
android:shape="oval">    
<stroke android:width="5dp" android:color="#ffff0000"/>    
</shape>
oval.PNG

如果我把定义宽和高相等的话就是圆形

<?xml version="1.0" encoding="utf-8"?>
<shape    xmlns:android="http://schemas.android.com/apk/res/android"    
android:shape="oval">    
    <stroke android:width="1dp" android:color="#ffff0000"/>    
    <size android:width="100dp" android:height="100dp"/>    
</shape>
circle.PNG

如果不用 Stroke( 描边,对应 Paint.STROKE ),而用Solid (填充,对应 Paint.FILL),会出现实心圆形。

circle_fill.PNG

填充的颜色也可以搞个渐变色

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"    
    android:shape="oval">    
<gradient        
    android:startColor="@color/colorAccent"    
    android:endColor="@color/colorPrimary"/>    
<size 
    android:width="100dp" 
    android:height="100dp"/>
</shape>
gradient_circle.png

矩形

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"    
    android:shape="rectangle">    
<corners 
    android:radius="10dp"/>    

<stroke 
    android:width="1dp" 
    android:color="@color/colorAccent"/>    
<size        
    android:width="100dp"        
    android:height="100dp"/>
</shape>
rectange.png

圆环

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"       
    android:innerRadius="100dp"       
    android:shape="ring"       
    android:thickness="10dp"       
    android:useLevel="false">    
<solid 
    android:color="@color/colorAccent"/>
</shape>
ring.PNG
  1. 这里要说下这个 useLevel, 只有在 LevelListDrawable 中才设置 true,这里要设置 false,否则显示不出来。
  1. 其实 GradientDrawable 或者说是 ShapeDrawable 的绘制原理就是利用 Canvas.drawCircle() ,Canvas.drawRect() 等等方法(Canvas 我将在后面的文章介绍)。

BitmapDrawable

BitmapDrawable 主要是用于图片的平铺( tileMode )显示,我们先看看没有设置 tileMode 的 BitmapDrawable

<?xml version="1.0" encoding="utf-8"?>
<bitmap 
    xmlns:android="http://schemas.android.com/apk/res/android"            
    android:antialias="true"        
    android:dither="true"        
    android:filter="true"       
    android:mipMap="true"         
    android:src="@drawable/sample_1">
</bitmap>

antialias:抗锯齿
dither:抗抖动,当位图的像素配置与屏幕不同时(例如 ARGB_8888 的位图和 RGB_565 的屏幕)
filter:过滤。当位图收缩或者拉伸,为了使外观平滑使用过滤
mipMap:一种图像处理技术 ,主要用于在图片缩小时,还要获得高质量图片。

BitmapDrawable.PNG

BitmapDrawable 的侧重功能属性在于 android:tileMode=["disabled" | "clamp" | "repeat" | "mirror"]

disable:不解释,默认不平铺
clamp:当位图小于实际显示区域的时候,边缘的颜色拉伸
repeat:当位图小于实际显示区域的时候,水平和垂直方向,重复显示图片
mirror:当位图小于实际显示区域的时候,水平和垂直方向,以镜像的方式(或者说对称的方式)重复显示图片

为了说明 tileMode ,先看一张原图

origin.PNG

repeat

repeat.PNG

mirror

mirror.PNG

Clamp

这个就有点意思了,我看到了一个实际用法(参考博客http://blog.csdn.net/u012702547/article/details/51594131

不会 PS 是硬伤,只能借用下别人的图了。但是我们主要学习这个实际中怎么应用。

原图是这样

clamp_origin.PNG

clamp 平铺后

clamp.PNG

把它应用到 TextView 背景后

<TextView    
    android:id="@+id/image_view"        
    android:layout_width="match_parent"     
    android:layout_height="match_parent"    
    android:background="@drawable/bitmapfile"      
    android:padding="40dp"    
    android:text="@string/bitmap_clamp_text"    
    android:textSize="20sp"/>
clamp_bg.PNG

LayerListDrawable

图层列表,就像我们平时把一张照片放在另外一张照片上,最后的照片在最上面。 LayerListDrawable 就是这样设计的。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">    
<item>        
    <shape android:shape="oval">            
        <solid android:color="@color/colorAccent"/>        
    </shape>    
</item>    
    <item android:width="64dp" android:height="64dp">        
        <bitmap           
            android:gravity="center"            
            android:src="@android:drawable/ic_input_add"/>      
    </item>
</layer-list>
LayerListDrawable.PNG

这个是不是有点像 FloatingActionButton,把这个设置为 Button 的背景,然后设置 android:elevation,是不是就 perfect了。

使用注意事项

  1. 我们用 <item> 控制显示的大小和位置,而用<item> 的子节点控制显示区域,如 <bitmap>,<shape>,<nine-path> 等等 Drawable
  2. 不用<item>显示图片因为图片会根据显示区域缩放,如果我们将图片放在子节点中,例如 <bitmap>,就算图片小于显示区域也不会放大。

StateListDrawable

在 MD 设计之前,大家应该有个印象,在点击像 ListView 的 Item 的时候,是没有波纹效果的,我们往往会自己设计一个简单的效果,就是点击 Item 的时候,背景切换个颜色。而这个就是用 StateListDrawable。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">    
  <item android:state_pressed="true">        
    <shape>            
        <corners android:radius="10dp"/>            
        <solid android:color="@color/colorAccent"/>              
    </shape>    
  </item>    

  <item>        
    <shape>            
        <corners android:radius="10dp"/>            
        <stroke 
            android:width="1dp"   
            android:color="#685252"/>        
    </shape>    
  </item>
</selector>

设置为 Button 的 background 后效果如下

StateListDrawable.gif

LevelListDrawable

根据等级(level)来显示图片。典型的应用就是 WIFI 信号。用 setLevel()来控制 level,根据 level 所在的范围显示相应的图片。

<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:maxLevel="0" android:drawable="@drawable/ic_wifi_signal_1" />
    <item android:maxLevel="2" android:drawable="@drawable/ic_wifi_signal_2" />
    <item android:maxLevel="5" android:drawable="@drawable/ic_wifi_signal_3" />
    <item android:maxLevel="9" android:drawable="@drawable/ic_wifi_signal_4" />
</level-list>

maxLevel = 0 就是默认图片,看下默认效果

default.PNG

现在给 ImageView 背景

    <ImageView
        android:layout_centerInParent="true"
        android:id="@+id/wifi_image"
        android:onClick="onClickWifi"
        android:background="@drawable/level_list_drawable"
        android:layout_width="200dp"
        android:layout_height="200dp"/>
    private static int i = 1;
    public void onClickWifi(View view) {
        ImageView imageView = (ImageView) findViewById(R.id.wifi_image);
        imageView.getBackground().setLevel(i++ % 10);
    }

现在效果,每点击一次 ImageView,level 改变一次。第一次点击 ,level 是1,显示 maxLevel="2" 图片,再点击一次,level 是2,还是显示 maxLevel="2" 图片,依此类推,出现如下效果

levelListDrawable.gif

TransitionDrawable

在两个图像之间,通过改变 alpha 来交替显示。

<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape>
            <solid android:color="@color/colorAccent"/>
        </shape>
    </item>

    <item>
        <bitmap android:src="@drawable/sample_1"/>
    </item>
</transition>

为 ImageView 设置这个背景

    <ImageView
        android:id="@+id/transition"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/transition_drawable"
        android:onClick="onClickTransition"/>

实现点击事件

    private boolean isReverse;
    public void onClickTransition(View view) {
        ImageView imageView = (ImageView) findViewById(R.id.transition);
        TransitionDrawable drawable = (TransitionDrawable) imageView.getBackground();
        if (!isReverse) {
            drawable.startTransition(2000);
            isReverse = true;
        } else {
            drawable.reverseTransition(2000);
            isReverse = false;
        }
    }
TransitionDrawable.gif

InsetDrawable

看这个命名,意思是 插入式的 Drawable。也就是说,可以按照指定的位置(left,top,right,bottom)来显示图片

<?xml version="1.0" encoding="utf-8"?>
<inset
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/sample_1"
    android:insetLeft="20dp"
    android:insetRight="10dp"
    android:insetTop="10dp"/>
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@color/colorAccent"
        android:src="@drawable/inset_drawable"/>
InsetDrawable.PNG

可以看到 left , top , right 有明显的 padding

ClipDrawable

从名字上看,意思是可以裁减的 Drawable。

<?xml version="1.0" encoding="utf-8"?>
<clip
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:clipOrientation="horizontal"
    android:drawable="@drawable/sample_1"
    android:gravity="center">
</clip>

clipOrientation 控制裁减的方向
gravity 控制从哪里开始裁减

布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <ImageView
        android:id="@+id/clip_image"
        android:background="@drawable/clip_drawable"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <SeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/seekBar2"/>
</LinearLayout>

Java Code

        final ImageView imageView = (ImageView) findViewById(R.id.clip_image);
        SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar2);
        seekBar.setMax(10000);
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                imageView.getBackground().setLevel(i);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
ClipDrawable.gif

ScaleDrawable

意思是可缩放的 Drawable。不过用起来有点费劲。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">


    <ImageView
        android:id="@+id/scale_image"
        android:layout_width="match_parent"
        android:layout_height="450dp"
        android:background="#FF4081"
        android:src="@drawable/scale_drawable"/>

    <SeekBar
        android:layout_alignParentBottom="true"
        android:id="@+id/seekBar3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"/>
</RelativeLayout>

scaleGravity 控制显示的位置

    <ImageView
        android:id="@+id/scale_image"
        android:layout_width="match_parent"
        android:layout_height="450dp"
        android:background="#FF4081"
        android:src="@drawable/scale_drawable"/>

基本设置已经完毕,但是就是不显示,我们需要控制 ScaleDrawable 的 Level,我们加入一个 SeekBar 控制 Level

        final ImageView imageView = (ImageView) findViewById(R.id.scale_image);
        SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar3);
        seekBar.setMax(10000);
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                imageView.getDrawable().setLevel(i);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
    }

level 控制缩小的比例的,level 值是从 0 到 10000,0代表不显示,从1开始控制图片放大,10000就是原图,看下面效果演示

ScaleDrawable.gif
  1. android:level 属性在 API 24 版本后,可以在 XML 文件中添加
  2. android:useIntrinsicSizeAsMinimum="true",这个设置为 true 后,就会一直显示原图的大小。
  3. 如果你想知道为什么,可以探究源码 ScaleDrawable.java 的 onBoundsChange() 方法

结束

通过本篇文章,我们要达到两点效果,一是 Drawable 是什么,二是为什么要用 Drawable。

由于篇幅原因,现在就介绍这么多,在下篇会把 NinePathDrawable,ColorDrawable,RoundBitmapDrawable,VectorDrawable 也加进来。同时在后面的文章中,我会介绍如何自定义 Drawable,让大家逐步感受到其实不用自定义 View 也能做很多事情。

如果大家有何疑问,可以正文评论,我会尽量解答,大家大同学习进步。


如果您喜欢这类文章 ,不妨点个赞,甚至可以关注一波我 不惜留恋_,感谢阅读~

推荐阅读更多精彩内容