Drawbale与Bitmap

1、Drawable与Bitmap对比

1.1、定义对比

Bitmap和Drawable定义时注定不是一个东西。
Bitmap成为位图,一般位图文件格式扩展名为.bmp,当然编码器也有很多,如RGB565、RGB8888。作为一种逐像素的显示对象,其执行效率高,但是其存储效率低。我们将其理解为一种存储对象比较好。
Drawable作为Android下通用的图像对象,它可以装载常用的格式的图像,比如GIF、PNG、JPG,当然也支持BMP,还提供了一些高级的可视化对象,比如渐变、图形等。
也就是说Bitmap是Drawable,但是Drawable却不一定是Bitmap。

1.2、指标对比

指标对比如下图


image.png

从上图可以看到,Drawable在占用内存和绘制速度上优于Bitmap,这也是Android UI系统中 使用Drawable的原因之一。

2、Bitmap

Bitmap在绘图中是一个非常重要的概念,在我们熟知的Canvas中就保存着一个Bitmap对象。我们调用Canvas各种绘图函数,最终都绘制在其中的Bitmap上。我们自定义View,重写其onDraw(Canvas canvas),这个Canvas就是通过Bitmap创建出来的。

2.1、Bitmap在绘图中的使用

Bitmap在绘图中的使用方式有两种:

  • 1、转换成BitmapDrawable使用
  • 2、当做画布使用

2.1.1、转换成BitmapDrawable使用

将Bitmap转换成BitmapDrawable对象,就可以作为Drawable使用了。

val bitmap=BitmapFactory.decodeResource(resources,R.mipmap.avator)
val bitmapDrawable=BitmapDrawable(bitmap)
iv.setImageDrawable(bitmapDrawable)

2.1.2、作为画布使用

方式一:使用默认画布

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawRect(rect,paint)
    }

这个Canvas看着和Bitmap没任何关系,其实这个Canvas是通过Bitmap创建的,通过Canvas绘制的图像都是绘制在Bitmap上的,这个Bitmap就是默认的画布。
方式二:创建画布
除重写onDraw(Canvas canvas)或dispatchDraw(Canvas canvas)之外,有时我们想在特定的Bitmap上作画,比如给照片增加水印,或者,我们只需要一张空白的画布,在这些情况下,我们就需要自己创建画布了。

Bitmap bitmap= Bitmap.createBitmap(200 , 100 , Bitmap.Config.ARGB_8888); 
Canvas canvas= new ·canvas(bitmap); 
canvas.drawColor(Color BLACK);

2.2、Bitmap格式

Bitmap是位图,也就是由一个个像素点组成的,这就涉及到两个问题:一、如何存储每个像素点;二、相关的像素点之间能否压缩。

2.2.1、如何存储每个像素点

一张位图所占的内存= 图片的宽(px)X图片的高(px)X每个像素点所占的字节数。在Android中,每个像素点所占的字节数是由Bitmap.Config中的各个参数来表示,Bitmap.Config有4个可选值:

  • 1、ALPHA_8:表示8位Alpha位图,只存储Alpha位不存储颜色,一个像素点占1个字节。
  • 2、ARGB_4444:表示16位位图,即A、R、G、B各占4位,一个像素点占2字节。
  • 3、ARGB_8888:表示32位位图,即A、R、G、B各占8位,一个像素点占4字节。
  • 4、RGB_565:表示16位位图,没有透明度,R占5位,G占6位,B占5位,一个像素点占2字节。
    ARGB_8888是最占内存的,也是画质最好的,如果对图片没有透明度要求,可以使用RGB_565替代ARGB_8888,这样会节省一半的内存。

2.2.2、如何计算Bitmap所占的内存

在计算Bitmap所占内存之前,需要明确一下:内存中存储的Bitmap对象和文件中存储的Bitmap图片不是一个概念。文件中存储的Bitmap图片是经过压缩算法压缩过的,而内存中的存储的Bitmap对象是通过BitmapFactory或Bitmap的Create方法创建的。它保存在内存中,它有明确的宽高,所以内存中Bitmap对象所占的内存大小=Bitmap的宽XBitmap的高X每个像素所占的内存大小。

2.2.3、Bitmap的压缩格式

如果要将Bitmap存储在硬盘上,那么必然存在如何压缩图片,在Android中压缩格式使用枚举类Bitmap.CompressFormat中的成员变量表示

  • Bitmap.CompressFormat.JPEG
  • Bitmap.CompressFormat.PNG
  • Bitmap.CompressFormat.WEBP
    其实这个参数很简单,就是指定 Bitmap是以JPEG、PNG 还是 WEBP 格式来压缩的,每种格式对应一种压缩算法。

2.3、使用BitmapFactory创建Bitmap

BitmapFactory是一个工具类,提供很多方法,可以从不同的数据源中获取Bitmap对象。函数如下

public static Btmap decodeResource(Resources res, int id) 
public static Bitmap decodeResource(Resources res, int id, Options opts) 

public static Bitmap decodeFile(String pathName) 
public static Bitmap decodeFile(String pathName , Options opts) 

public static Bitmap decodeByteArray(byte[] data, int offset, int length) 
public static Bitmap decodeByteArray(byte[] data , int offset, int length , 
Options opts) 

public static Bitmap decodeFileDescriptor(FileDescriptor fd) 
public static Bitmap decodeFileDescriptor (FileDescriptor fd , Rect outPadding, 
Options opts) 

public static Bitmap decodeStream(InputStream is) 
public static Bitmap decodeStream(InputStream is, Rect outPadding , Options 
opts) 

public static Bitmap decodeResourceStream(Resources res, TypedValue 
value , InputStream is, Rect pad, Options opts)

单从这些函数中就可以看出, BitmpFactory 的功能很强大,可以针对资源、文件、字节数组、FileDescriptor、InputStream 数据流解析出对应的 Bitmap 对象,如果解析不出来则返回null。

2.3.1、decodeResource(Resources res, int id)

表示从资源中解码一张位图

val bitmap=BitmapFactory.decodeResource(resources,R.mipmap.avator)
val bitmapDrawable=BitmapDrawable(bitmap)
iv.setImageDrawable(bitmapDrawable)

2.3.2、decodeFile(String pathName)

表示从文件路径加载图片,注意:pathName必须是全路径名

String fileName = ” / data /data/demo.jpg”; 
Bitmap bm = BitmapFactory . decodeFile(fileName) ; 
if (bm == null) { 
//TODO 文件不存在

2.3.3、decodeByteArray(byte[] data, int offset, int length)

这个函数根据byte数组解析出Bitmap。

  • byte[] data:压缩图像数据的字节数组
  • int offset:图像数据的偏移量,用于解码器定位从哪里开始解析
  • int length:字节数,从偏移量开始,指定取多少字节进行解析
    它的一般使用步骤如下:
  • 1、开启异步线程去获取图片
  • 2、网络返回InputStream
  • 3、将InputStream转成Byte数组
  • 4、调用decodeByteArray()将字节数组转成Bitmap
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        thread {
            try {
                val byteArray: ByteArray? = getImage("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1608470434007&di=eacadf647e186ccc84b79471779066ad&imgtype=0&src=http%3A%2F%2Fimage.huahuibk.com%2Fuploads%2F20190107%2F21%2F1546866811-wczJFfBvDb.jpg")
                byteArray?.let {
                    val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
                    runOnUiThread {
                        iv.setImageBitmap(bitmap)
                    }
                }

            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

    }

    private fun getImage(path: String): ByteArray? {
        val url = URL(path)
        val urlConnection: HttpURLConnection = url.openConnection() as HttpURLConnection
        urlConnection.requestMethod = "GET"
        urlConnection.readTimeout = 5 * 1000
        urlConnection.connect()
        if (urlConnection.responseCode == 200) {
            val inputStream = urlConnection.inputStream
            val result = readStream(inputStream)
            inputStream.close()
            return result
        }
        return null
    }

    private fun readStream(inputStream: InputStream): ByteArray {
        val outStream = ByteArrayOutputStream()
        val buffer = ByteArray(1024)
        var len = inputStream.read(buffer)
        while (len != -1) {
            outStream.write(buffer,0,len)
            len=inputStream.read(buffer)
        }
        return outStream.toByteArray()
    }
}

上面代码readStream()方法比较特殊,方法中先将InputStream的数组读到Btye数组中,然后再讲Byte数组中的数据写到OutPutStream中,最后通过outStream.toByteArray()返回Byte数组。这里直接将InputStream中的数据读到Byte数组中,直接返回这个数组不可以吗?为啥还要将Byte数组写到OutPutStream中,再通过outStream.toByteArray()返回呢?
这是因为BitmapFactory.decodeByteArray()函数中所需的字节数组并不是想象中的数组,而是把输入流转换为字节内存输出流的字节数组格式。如果不经过OutputStream转换,直接将从InputStream中读取的Byte数组返回,那么 decodeByteArray() 函数将一直返回 null。

2.3.4、decodeFileDescriptor

decodeFileDescriptor有两个构造函数

public static Bitmap decodeFileDescriptor(FileDescriptor fd)
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)

参数

  • FileDescriptor fd:包含解码位图数据的文件路径
  • Rect outPadding:用于返回矩形的内边距。如果Bitmap没有被解析成功,则返回(-1,-1,-1,-1)。如果不需要传入null即可,这个参数一般不使用。
    使用 decodeFileDescriptor 的示例代码如下:
String path ="/data/data/demo.jpg"; 
FileIputStream is== new FileInputStream(path); 
bmp = BitmapFactory. decodeFileDescriptor (is . getFD ()); 
if (bm == null) { 
//TODO 文件不存在

上面代码是根据FileDescriptor对象解析出对应的Bitmap,而FileDescriptor的一般后去方式通过FileInputStream获取的。既然根据路径创建了FileInputStream,那么为什么不直接使用BitmapFactory.decodeFile()来解析图片呢?
这是因为 BitmapFactory. decodeFileDescriptor的性能要优于BitmapFactory.decodeFile(),下面通过源码看下区别。
首先来看BitmapFactory. decodeFileDescriptor的源码:


image.png

从源码可以看出BitmapFactory. decodeFileDescriptor直接调用native方法,解析出Bitmap并返回。
再来看decodeFile源码


image.png

可以看到decodeFile最终会调用decodeStream()
image.png

在调用nativeDecodeStream()底层方法之前还多申请了两次内存,所以说decodeFileDescriptor的性能优于decodeFile()方法。

2.3.5、decodeStream

decodeStream有两个构造函数

public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)

参数

  • InputStream is:用于解码位图的输入流
  • Rect outPadding:和decodeFileDescriptor的参数含义一样,用于返回矩形的内边框。
    这个函数用于加载网上的图片。

2.4、BitmapFactory.Options

通过这个参数可以设置Bitmap的采样率,通过改变图片的宽度、高度、缩放比例等,已达到减少图片的像素的目的,减少使用中的OOM。
下面看下BitmapFactory.Options的常用的成员变量:

public boolean inJustDecodeBounds;
public boolean inSampleSize;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity;
public boolean inScaled;
publ Bitmap.Config inPreferredConfig;
public int outWidth
public int outHeight;
public String outMimeType ;

上面成员变量中,以in开头的表示设置参数,以out开头的表示获取参数。

2.4.1、inJustDecodeBounds是否加载图片

inJustDecodeBounds设置为true,表示只获取图片信息,不获取图片,不加载内存。能获取的信息有图片的宽度、高度和图片的MIME类型。图片的宽度、高度通过Options.outWidth、Options.outHeight返回,图片的MIME类型通过Options.outMimeTyepe返回。
这个参数通常用于图片压缩,当图片过大时我们需要获取图片的信息并且不加载图片到内存,否则可能会OOM。使用这个参数就能达到我们的目的。

2.4.2、inSampleSize压缩图片

这个参数表示采样率,采样率是指每隔多少像素,获取一个像素作为结果。比如inSampleSize设置为4,就会从原图片中每4个像素中取一个像素,这样图片的宽和高都变为原来的1/4。
针对inSampleSize官方建议取2的次幂,否则会被系统向下取整取一个最接近的值。不能取小于1的值,否则一直使用1来作为采样率。
这个参数主要用于图片的压缩,那么如何确定一张推按的采样率呢?
比如ImageVIew的大小为100px X 100px,图片的大小为300px X 400px,此时inSampleSize应该是多少呢?
通过计算可知,图片的宽是目标宽度的3倍,图片的高是目标高度的4倍,如果inSampleSize设置为4,那么图片的宽高将变为75px X 100px,图片显示在ImageView中宽度需要拉伸,拉伸可能会导致图片失真,本身使用inSampleSize就是一种失真的压缩方式。所以我们应该设置inSampleSize为3。所以选取inSampleSize的标准是尽可能使缩放后的图片的宽高尽可能大于等于目标宽高,以至于不会失真太多于严重。

2.4.3、加载一个Bitmap文件究竟占多少内存

前面小节中,我们说过Bitmap的内存=Bitmap的宽 X Bitmap的高 X 一个像素点占的字节数。那么这里的Bitmap的宽高是不是就等于Bitmap文件在图片浏览工具中显示的宽高呢?
其实不然,Android系统在加载Bitmap时会动态的缩放,缩放后的的宽高,才是计算内存时真正的宽高。
如果加载的图片是在资源文件夹下比如drawable、mipmap-hdpi、mipmap-mdpi、mipmap-xhdpi、mipmap-xxhdpi、mipmap-xxxhdpi,会根据文件夹对应的像素密度和当前设备的像素密度进行计算,根据计算结果进行缩放。
如果加载的图片是在SD卡中,Android系统是不进行缩放的。

2.4.4、inScaled

当从资源文件中加载图片到内存中时,会根据当前屏幕的像素密度和图片所在资源文件夹对应像素密度进行计算来决定是否缩放。如果设置inScaled=false的话,那么就不会进行缩放了。

2.4.5、 inDensity inTargetDensity inScreenDensity

  • inDensity :用于设置文件所在资源文件夹的分辨率
  • inTargetDensity:用于设置真实显示的屏幕分辨率
  • inScreenDensity:并没有什么卵用,源码中未用到
    一张图片的缩放比例=屏幕的真实分辨率/所在资源文件夹对应的分辨率,所以我们可以通过设置文件所在资源文件夹的分辨率和屏幕的真实分辨率手动指定缩放的比例。

2.4.6、inPreferredConfig

这个参数用于设置像素的存储格式的,在讲 Bitmap.Config 时提到,图片的像素存储格式有ALPHA_8、RGG_565、ARGB_4444、ARGB_8888 默认使用 ARGB_8888。通过设置像素的存储格式也能对图片进行压缩。

2.5、创建Bitmap的方法之:Bitmap的静态方法

2.5.1、createBitmap(int width, int height, Bitmap.Config config)

表示创建一个空白的图像

  • width、height 用于指定所创建空白图像的尺寸,单位是 px
  • config 用于指定单个像素的存储格式.

2.5.2、createBitmap(Bitmap src)

这个函数的作用就是创建一个和src一样的Bitmap对象。

2.5.3、createBitmap(Bitmap source ,int x, int y, int width, int height)

这个函数用于裁剪图像,参数如下:

  • Bitmap source:用于裁剪的源图像
  • x,y:开始裁剪的位置坐标
  • width、height:裁剪的宽度和高度
    下面使用函数把下面图片中的小狗裁剪出来


    image.png

    代码如下

val dogBitmap = BitmapFactory.decodeResource(resources, R.mipmap.dog)
val bitmapWidth = dogBitmap.width
val bitmapHeight = dogBitmap.height
val cropBitmap = Bitmap.createBitmap(dogBitmap,bitmapWidth/3,bitmapHeight/3,bitmapWidth/3,bitmapHeight/3)
iv.setImageBitmap(cropBitmap)

效果图如下


image.png

2.5.4、createBitmap(Bitmap source,int x, int y, int width , int height, Matrix m , boolean filter)

这个函数比上面的裁剪函数多了两个参数,它的作用是给裁剪后的图片添加矩阵。

  • Matrix m :给裁剪后的图片添加矩阵
  • boolean filter:对应paint.setFilterBitmap(filter)表示是否给图像添加滤波效果。如果设置为 true ,则能够减少图像中由于噪声引起的突兀的孤立像素点或像素块。
    依然使用上面的例子,只不过使裁剪的效果高度变成裁剪后的2倍,宽度不变。
val dogBitmap = BitmapFactory.decodeResource(resources, R.mipmap.dog)
val bitmapWidth = dogBitmap.width
val bitmapHeight = dogBitmap.height
val matrix = Matrix()
matrix.setScale(2f, 1f)
val cropBitmap = Bitmap.createBitmap(
    dogBitmap,
    bitmapWidth / 3,
    bitmapHeight / 3,
    bitmapWidth / 3,
    bitmapHeight / 3,
    matrix,
    true
)
iv.setImageBitmap(cropBitmap)

效果如下


image.png

2.5.5、createScaledBitmap(Bitmap src, int dstWidth , int dstHeight, boolean filter)

该函数用于创建缩放的位图
参数如下:

  • Bitmap src:需要进行缩放的源位图
  • dstWidth 、dstHeight:缩放后的目标高度
  • boolean filter:是否给图像添加滤波效果,对应paint.setFilterBitmap(filter)
    下面举例看下用法:
val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.scenery)
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, 300, 200, true)
iv.setImageBitmap(scaledBitmap)

上面代码是将图片缩放到宽为300px,高位200px。

2.6、Bimtap的常用函数

2.6.1、copy(Config config,boolean isMutable)

该函数的作用是创建一个源图像的一个副本,但是可以更改像素的存储格式。

  • Config config:像素的存储格式,取值可为ALPHA_8、ARGB_4444、ARGB_8888、RGB_565。
  • boolean isMutable:新创建的Bitmap是否可更改其中的像素。
    我们可以通过如下函数判断Bitmap的像素是否可更改
public final boolean isMutable()

返回true表示可更改,返回false表示不可更改。如果Bitmap中的像素是不可更改的话,还非要使用setPixel()方法设置其像素的话,会报如下错误。

Caused by: 
  java.lang.IllegalStateException
  at android.graphics.Bitmap.setPixel(Bitmap.java:2020)
  at com.example.valueanimatortest.MainActivity.onCreate(MainActivity.kt:18)

那么问题来了:通过哪种方式加载的Bitmap是像素可更改的,哪些是像素不可更改的?
通过BitmapFactory加载的Bitmap是像素不可更改的,通过Bitmap中的几个函数创建的Bitmap是像素可变的。这些函数如下:

copy(Bitmap.Config config , boolean isMutable) 
createBitmap(int width ,int height , Bitmap.Config config) 
createScaledBitmap(Bitmap src , int dstWidth , int dstHeight, boolean filter) 
API 17 中引入
createBitmap (DisplayMetrics display, int width , int height , Bitmap . Config config)

使用createScaledBitmap()时需要注意的是:createScaledBitmap()是创建缩放Bitmap的,当源图像的宽高和目标图像的宽高相同时,不会生成新的Bitmap,会将源图像返回。如果源图像是像素不可更改的,那么返回的Bitmap也是像素不可更改的。
还有一点需要注意:当Bitmap是像素不可更改的,那么该Bitmap是不能作为画布的,否则会报错

Caused by:
java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor
at android.graphics.Canvas.<init>(Canvas.java:117)
at com.example.valueanimatortest.MainActivity.onCreate(MainActivity.kt:18)

2.6.1、extractAlpha()

这个函数的作用是从Bitmap中抽取Alpha值,生成一个只含Alpha值的图像,像素存储格式为ALPHA_8。
函数声明如下:

public Bitmap extractAlpha()
public Bitmap extractAlpha(Paint paint, int[] offsetXY) 

extractAlpha()
该函数的作用是获取源图像的Alpha信息,生成一个只含Alpha值的图像,像素的存储格式为ALPHA_8。
使用此函数实现下图效果

extractAlpha.png

代码如下

val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.cat_dog)
val paint = Paint()
paint.color = Color.CYAN
//抽取alpha通道
val alphaBitmap = bitmap.extractAlpha()
//创建一个和alphaBitmap大小相同的画布
val canvasBitmap =
    Bitmap.createBitmap(alphaBitmap.width, alphaBitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(canvasBitmap)
canvas.drawBitmap(alphaBitmap,0f,0f,paint)
iv.setImageBitmap(canvasBitmap)

canvas.drawbitmap(),这个函数的paint参数一般是没什么用的,因为图片中包含透明通道和颜色信息,对于一个含有完整ARGB信息的Bitmap而言,paint的作用就是把图像画到画布上,paint中的颜色对画的Bitmap没有任何影响。而如果画到Canvas上的Bitmap只含有Alpha通道,那么画到画布上的图像的颜色就是用画笔的颜色填充的。
**extractAlpha(Paint paint, int[] offsetXY) **

  • Paint paint:具有MaskFilter效果的Paint,一般使用BlurMaskerFilter模糊效果。
  • int[] offsetXY:返回添加BlurMaskerFilter模糊效果后的原点的偏移量。比如我们使用一个半径为6的BlurMaskerFilter,那么源图像模糊之后,图像的上下左右就会多出6px的模糊效果。所以要想完全显示这幅图像,就不应该从源图像的左上角(0,0)开始绘制,而应从(-6,-6)开始绘制,而offsetXY就是相对图像的建议绘制起始点位置,所以offsetXY={-6,-6}。注意,offsetXY只是建议起始位置,不一定和BlurMaskerFilter的模糊半径一致。
    仍然使用上述图片,给它增加模糊效果。


    模糊效果.png

    首先根据源图像抽出Alpha图像

val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.cat_dog)
val paint = Paint()
paint.maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL)
paint.color = Color.CYAN
val offsetXY = IntArray(2)
val alphaBitmap = bitmap.extractAlpha(paint, offsetXY)
Log.e(javaClass.simpleName, "offsetX is ${offsetXY[0]} offsetY is ${offsetXY[1]}")

日志输出:

MainActivity: offsetX is -9 offsetY is -9

上面代码很简单,创建一个BlurMaskFilter对象设置给paint,将实例化后的整形数组传递给方法extractAlpha(),返回个Alpha图像。
然后创建一个和Alpha图像大小相同的画布,设置paint颜色为蓝色,将Alpha图像画到画布上,这样Alpha图像的颜色就被填充为蓝色了。

val canvasBitmap =
        Bitmap.createBitmap(alphaBitmap.width, alphaBitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(canvasBitmap)
canvas.drawBitmap(alphaBitmap, 0f, 0f, paint)
iv.setImageBitmap(canvasBitmap)

在此基础上实现给图片增加纯色阴影的效果(图片的阴影和文字、图形阴影不同,图片的阴影是通过对图片边缘进行模糊实现的,无法通过设置纯色的阴影)


image.png

完整代码如下:

val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.cat_dog)
val paint = Paint()
paint.maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL)
paint.color = Color.CYAN
val offsetXY = IntArray(2)
val alphaBitmap = bitmap.extractAlpha(paint, offsetXY)
Log.e(javaClass.simpleName, "offsetX is ${offsetXY[0]} offsetY is ${offsetXY[1]}")
val canvasBitmap =
    Bitmap.createBitmap(alphaBitmap.width, alphaBitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(canvasBitmap)
canvas.drawBitmap(alphaBitmap, 0f, 0f, paint)
paint.maskFilter = null
canvas.drawBitmap(bitmap, -offsetXY[0].toFloat(), -offsetXY[1].toFloat(), paint)
iv.setImageBitmap(canvasBitmap)

相比上面代码之多了下面这句:

canvas.drawBitmap(bitmap, -offsetXY[0].toFloat(), -offsetXY[1].toFloat(), paint)

就是在模糊背景绘制出来以后,把源图像绘制上去。正因为我们的画布大小与alpha图像大小相同,所以可以反推出,-offsetXY[0],-offsetXY[1]所对应的坐标就是源图像原来所在的位置。
实例:实现单击描边效果
效果如下:

单击描边.gif

实现原理很简单,就是通过extractAlpha()生成纯色背景,然后在单击时将纯色背景作为ImageView的背景显示即可。
实现这个效果需要解决如下两个问题:

  • 1、如何设置单击事件

可以动态的给ImageView设置一个selector标签,在单击的时候应用我们生成的图像作为背景即可。因为selector标签对应的Drawable为StateListDrawable,只需要通过setBackground()将其设置为ImageView的背景即可。

  • 2、怎么样才能让背景被要显示的源图像大

通过ImageView.setBackground()设置背景,会忽略源图像的padding。所以只需要给ImageVIew设置padding,就能实现背景大于源图像的效果。
首先创建一个派生自ImageView的自定义控件,并在onFinishInflate()方法中,向ImageView添加背景。onFinishInflate()的调用时机是系统从XML中解析出对应控件实例的时候,这时候控件已经生成,但是还没被使用。所以需要对控件进行基础设置,则是最佳时机。

class StrokeImageView(context: Context, attrs: AttributeSet?) : AppCompatImageView(context, attrs) {

    private val mPaint = Paint()

    constructor(context: Context) : this(context, null)

    override fun onFinishInflate() {
        super.onFinishInflate()
        if (drawable is BitmapDrawable) {
            //获取源图像
            val bitmapDrawable = drawable as BitmapDrawable
            var srcBitmap = bitmapDrawable.bitmap
//srcBitmap=BitmapFactory.decodeResource(resources,R.mipmap.cat)
            //抽取Alpha图像
            val alphaBitmap = srcBitmap.extractAlpha()
            mPaint.color = Color.CYAN
            //创建和AlphaBitmap大小相同的画布
            val pressedBitmap =
                Bitmap.createBitmap(srcBitmap.width, srcBitmap.height, Bitmap.Config.ARGB_8888)
            val canvas = Canvas(pressedBitmap)
            //给Alpha填充颜色
            canvas.drawBitmap(alphaBitmap, 0f, 0f, mPaint)
            //添加状态
            val stateListDrawable = StateListDrawable()
            stateListDrawable.addState(
                intArrayOf(android.R.attr.state_pressed),
                BitmapDrawable(resources,pressedBitmap)
            )
            background = stateListDrawable
        }
    }

}

这里主要讲下StateListDrawable的使用

val stateListDrawable = StateListDrawable()
stateListDrawable.addState(
    intArrayOf(android.R.attr.state_pressed),
    BitmapDrawable(resources,pressedBitmap)
)

addState()是最基本的方式,用于添加对应状态的Drawable资源。函数声明如下:

public void addState(int[] stateSet, Drawable drawable)

参数如下:

  • int[] stateSet:填写对应的状态数组
  • Drawable drawable:这些状态对应的Drawable资源
    比如下面这个selector的标签item
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@mipmap/cat" android:state_enabled="true" android:state_pressed="true" />
    <item android:drawable="@mipmap/avator" android:state_enabled="false" android:state_pressed="false" />
</selector>

对应addState的代码如下

val stateListDrawable = StateListDrawable()
stateListDrawable.addState(
    intArrayOf(
        android.R.attr.state_pressed,
        android.R.attr.state_enabled
    ),ContextCompat.getDrawable(this,R.mipmap.cat)
)
stateListDrawable.addState(intArrayOf(-android.R.attr.state_pressed,-android.R.attr.state_enabled),ContextCompat.getDrawable(this,R.mipmap.avator))
iv_test.background=stateListDrawable

下面看下如何使用StrokeImageView

<com.example.valueanimatortest.StrokeImageView
    android:id="@+id/iv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@mipmap/cat"
    android:padding="3dp"
    android:clickable="true"
    android:focusable="true" />

ImageView默认是不可点击的,需要给ImageView添加clickable=true,给ImageView设置padding就可以达到源图像大小小于背景的效果,实现描边的效果。
最后说一下,上面代码并不能实现我们最终想要的功能,看下上面的代码:

//获取源图像
val bitmapDrawable = drawable as BitmapDrawable
var srcBitmap = bitmapDrawable.bitmap

通过ImageView.getDrawable(),然后通过BitmapDrawable.getBitmap()得到的Bitmap的宽高是Bitmap文件的宽高,并不是缩放后的宽高,而加载到内存中的Bitmap是需要根据屏幕的分辨率和图片所在资源文件夹下对应的分辨率计算出缩放比例,进行缩放的,所以使用这个Bitmap创建的画布是不正确的,效果如下:


image.png

2.6.2、分配空间的获取

获取Bitmap分配空间有三个函数

public final int getAllocationByteCount()

获取Bitmap分配的内存空间,是在API19上引入的,所以如果是在API19以上的机器就需要使用这个函数获取。

public final int getByteCount()

该函数也是获取Bitmap分配的内存空间,是在API12引入的,在API>12,API<19时使用这个函数获取。

public final int getRowBytes() 

获取每行分配的内存空间,Bitmap所占内存=getRowBytes() X Bitmap.getHeight(),即所占内存等于每行所占内存乘以行数。是在API1引入的,所以在API<12时,使用该函数获取。

fun getBitmapSize(bitmap: Bitmap) =
        when {
            Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT -> bitmap.allocationByteCount
            Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR1 -> bitmap.byteCount
            else -> bitmap.rowBytes * bitmap.height
        }

如果是在同时可用的平台上,比如在API20的机器上,上面三种方法得到的值是相同的。

val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.cat)
Log.e(javaClass.simpleName,"allocationByteCount is ${bitmap.allocationByteCount}")
Log.e(javaClass.simpleName,"byteCount is ${bitmap.byteCount}")
Log.e(javaClass.simpleName,"rowBytes is ${bitmap.rowBytes * bitmap.height}")

输出日志

E/MainActivity: allocationByteCount is 2196288
E/MainActivity: byteCount is 2196288
E/MainActivity: rowBytes is 2196288

2.6.3、isRecycled()/recycle()

public final boolean isRecycled()

是判断Bitmap是否被回收了,如果使用被回收的Bitmap是会报错的

public void recycle()

表示回收Bitmap。
在API10(Android2.3.3)之前,Bitmap的像素级数据被存在Native内存空间的。这些数据与Bitmap本身是隔离的,Bitmap本身被放在Dalvik堆中。我们无法预测Native层存储的像素级数据,这就意味着容易超过内存限制发生崩溃。在Android10之后,像素级数据和Bitmap本身都放在了Dalvik堆中,可以通过Java回收机制进行自动回收了。
在Android10以后,如果依然使用recycle()函数触发Bitmap回收,很可能会导致使用已回收的Bitmap带来的崩溃,所以在Android10之前,必须强制使用recycle()释放内存,在Android10以后,不再强制使用recycle()释放内存。

2.6.4、setDensity、getDensity()

在BitmapFactory中,我们讲过几个Density值,如inDensity、inTargetDensity。而这里Bitmap的setDensity()、getDensity()对应的就是inDensity。
inDensity表示该Bitmap适合的屏幕dpi,当目标屏幕的dpi(inTargetDensity)不等于它时,就会进行缩放。
各函数的声明如下:

public void setDensity(int density) 

参数density对应inDensity,用于设置图像建议的屏幕尺寸。

public int getDensity()

获取Bitmap的Density
下面举个例子,先获取Bitmap的原始Density,然后将Density放大两倍,这样在屏幕分辨率不变的情况下,图片就该缩小一半

val bitmap=BitmapFactory.decodeResource(resources,R.mipmap.dog)
iv1.setImageBitmap(bitmap)
bitmap.density=bitmap.density*2
iv.setImageBitmap(bitmap)

其中ImageView布局代码如下:

<ImageView
        android:id="@+id/iv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:scaleType="center" />
<ImageView
    android:id="@+id/iv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:scaleType="center" />

效果如下


image.png

需要注意的是,这里每个ImageView的scaleType必须设置为center。从效果图中可看出,在将Bitmap的density增大2倍之后,显示出来的图像明显缩小了一半。但是在增大Density之后,Bitmap在内存中的大小是没有变化的,所以这种方式只会影响显示的缩放,并不会改变Bitmap在内存中的尺寸。而通过BitmapFactory.Options设置inDensity时,加载到内存中的Bitmap的尺寸就已经被放大或缩小了。
另外在设置了Density之后,如果想用ImageView显示,那么就需要layout_width和layout_height属性设置为wrap_content,scaleType属性设置为center,才能完整的显示经过设置Density之后,在屏幕中被缩放的图像。如果scaleType设置成其他属性的话,会进行缩放,不能做到原样显示。
如果将设置Density的Bitmap作为View的背景的话,会自动缩放到控件大小显示,这样也不能看到缩放的效果的。

2.6.5、setPixel()、getPixel()

这两个函数是针对某个位置的像素进行设置和获取,函数声明如下

public void setPixel(int x, int y, int color)

该函数表示对指定位置的像素进行颜色设置。很明显,参数x,y表示像素点在Bitmap中的像素级坐标,参数color对应设置的颜色。

public int getPixel(int x, int y)

表示获取指定位置像素的颜色值。
下面举个例子,图片中绿色较多,只将绿色通道增大30
效果如下图

10_5_27.png

代码如下

val bitmap = BitmapFactory.decodeResource(resources, R.mipmap.dog)
iv1.setImageBitmap(bitmap)
val copyBitmap=bitmap.copy(bitmap.config,true)
for (x in 0 until copyBitmap.width) {
    for (y in 0 until  copyBitmap.height) {
        val originColor=copyBitmap.getPixel(x,y)
        val red= Color.red(originColor)
        var green=Color.green(originColor)
        val blue=Color.blue(originColor)
        val alpha=Color.alpha(originColor)

        if(green<200)
            green+=30
        copyBitmap.setPixel(x,y,Color.argb(alpha,red,green,blue))
    }
}
iv.setImageBitmap(copyBitmap)

其中有一点需要注意的是,通过BitmapFactory.decodeResource加载进来的Bitmap的像素是不可变的,所以需要使用Bitmap.copy()创建一个源图像副本,才可以通过setPixel()设置某个位置像素的颜色值。

2.6.6、compress

这个函数是用于压缩图像的,它会将压缩过的Bitmap写入到指定的输出流中,函数声明如下

public boolean compress(CompressFormat format, int quality, OutputStream stream)

参数如下

  • CompressFormat format:Bitmap的压缩格式,取值为CompressFormat.JPEG、CompressFormat.PNG、CompressFormat.WEBP这三种格式。WEBP是从API14才开始引入的。
  • int quality:压缩的质量,取值范围为0~100。0表示最低画质压缩,100表示最高画质压缩。对于压缩格式为PNG,会忽略此参数。
  • OutputStream stream:Bitmap压缩后,会以OutputStream形式在这里输出。
  • 返回值为布尔型,true表示压缩成功,false表示压缩失败。
    压缩格式
  • CompressFormat.JPEG:是一种有损压缩格式,即在压缩过程中改变图像的原本质量。compress函数中quality越小,画质越差,对图片原有质量损伤越大,但是得到图片的文件越小。而且JPEG不支持透明度,当遇到透明度像素时,会以黑色背景填充。
  • CompressFormat.PNG:是一种无损压缩格式,而且支持透明度。
  • CompressFormat.WEBP:在14<=API<=17时,是一种有损压缩格式,而且不支持透明度,在API18以后,是一种无损压缩格式,而且支持透明度。在有损压缩时,在质量相同的情况下,WEBP图像的体积比JPEG格式图像体积小40%,但是WEBP格式图像的编码时间要比JPEG格式的编码时间长8倍。在无损压缩时,无损的WEBP图片比PNG图片小26%,但是WEBP格式的压缩时间比PNG格式的压缩时间长5倍。
    示例:使用JPEG压缩格式压缩图片
val bitmap =BitmapFactory.decodeResource(resources,R.mipmap.cat)
iv1.setImageBitmap(bitmap)
Log.e(javaClass.simpleName,"压缩前,占内存大小:${bitmap.allocationByteCount} ")

val outPutStream1= ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG,10,outPutStream1)
val byteArray1=outPutStream1.toByteArray()
val bitmap1=BitmapFactory.decodeByteArray(byteArray1,0,byteArray1.size)
iv.setImageBitmap(bitmap1)
Log.e(javaClass.simpleName,"压缩后,占内存大小:${bitmap1.allocationByteCount} 二进制数据长度:${outputStream1.toByteArray().size}")

val outPutStream2= ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG,20,outPutStream2)
val byteArray2=outPutStream2.toByteArray()
val bitmap2=BitmapFactory.decodeByteArray(byteArray2,0,byteArray2.size)
iv.setImageBitmap(bitmap2)
Log.e(javaClass.simpleName,"压缩后,占内存大小:${bitmap2.allocationByteCount} 二进制数据长度:${outputStream2.toByteArray().size}")

打印的log如下

E/MainActivity: 压缩前,占内存大小:717000 二进制数据长度:7189
E/MainActivity: 压缩后,占内存大小:717000 二进制数据长度:9612

发现压缩格式为JPEG时,quality的大小并不会改变Bitmap所占内存大小,quality越小,二进制数据的长度就越小。
显示效果如下:

image.png

从效果图中可以看出压缩后的 JPEG 图像质量很差,己经具有明显颜色块了。
源图像从上到下是具有 alpha 渐变的,而在生成 JPEG 图像时,完全以黑色背景显示整幅图像且完全没有 Alpha 效果。 面我 已经讲过 JPEG 压缩算法是有损压缩,不支持 alph当遇到透明度像素 ,会使用黑色背景填充。
当在API 17 平台上生成 种格式的压缩图像时,效果如下图所示( quality= 1)
image.png

从图像中可以明显看出,PNG 图像是无损压缩的,它压缩后的图像跟源图像一模一样。JPEG WEBP 图像是有损压缩的,而且都不支持透明通道,但是在 quality一致的情况下,WEBP 图像明显要比 JPEG 图像质量高。当然,只有在 14<=API<=17 时, WEBP 才是有损压缩格式,效果才是这样的,而在 API18 以后,同样在 quality= 1的情况下,效果如下图所示
image.png

很明显,在 API18 后,WEBP PNG一样,都是无损压缩格式。
**总结:

  • 1、当压缩格式为PNG,它是无损压缩的,改变quality不会影响bytesArray二进制数据的长度。
  • 2、当压缩格式为JPEG,它是有损压缩的,且不支持透明通道,quality越小,bytesArray二进制数据的长度就越小。
  • 3、在14<=API<=17,压缩格式为WEBP时,它是有损压缩的,在质量相同的情况下,压缩后的图像体积比压缩格式为JPEG格式的图像体积小40%。
  • 4、在API18以后,压缩格式WEBP时,它是无损压缩的,在质量相同的情况下,压缩后的图像体积比压缩格式为PNG格式的图像体积小26%**
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容