Android开发Android进阶之路Android开发

Android必知必会——Drawable

2020-05-20  本文已影响0人  不正经的创造者

Drawable概览

如果需要在应用内显示静态图片,可以使用 Drawable 类及其子类绘制形状和图片。Drawable 是可绘制对象的常规抽象。不同的子类可用于特定的图片场景,可以对其进行扩展以定义行为方式独特的可绘制对象。

Drawable的定义和实例化

可以通过如下三种方式定义和实例化Drawable:

val myImage1: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, null)

val myImage2: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.my_image, null)

myImage1.setAlpha(1)//myImage2也会受到影响

<transition xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@drawable/image_expand">
        <item android:drawable="@drawable/image_collapse">
    </transition>

VectorDrawable

VectorDrawable 是一种矢量图形,在 XML 文件中定义为一组点、线条和曲线及其相关颜色信息。使用矢量可绘制对象的主要优势在于图片可缩放。可以在不降低显示质量的情况下缩放图片,也就是说,可以针对不同的屏幕密度调整同一文件的大小,而不会降低图片质量。这不仅能缩减 APK 文件大小,还能减少开发者维护工作。还可以对动画使用矢量图片,具体方法是针对各种显示屏分辨率使用多个 XML 文件,而不是多张图片。

VectorDrawable 定义静态可绘制对象。与 SVG 格式类似,每个矢量图形定义为树状层次结构,由 path 和 group 对象构成。每个 path 都包含对象轮廓的几何图形,而 group 包含转换的详细信息。所有路径都是按照其在 XML 文件中显示的顺序绘制的。


借助 Vector Asset Studio 工具,可轻松地将矢量图形作为 XML 文件添加到项目中。

对于矢量图形,还有另外一个类AnimatedVectorDrawable,它 可以为矢量图形的属性添加动画。

Android5.0开始支持使用矢量图形,如果要在更低版本使用,那么可以通过VectorDrawableCompat 和 AnimatedVectorDrawableCompat来进行兼容。在控件中,如ImageView,可以使用srcCompat属性,来引用矢量图形。

Bitmap

Bitmap是一种独立于显示器的位图数字图像文件格式。BMP文件通常是不压缩的,所以它们通常比同一幅图像的压缩图像文件格式要大很多。

Bitmap存储的核心,在于图像的信息。即宽度上有多少像素点,高度上有多少像素点,然后每个像素点的具体信息。

而针对每个像素点,通常保存的颜色深度有2(1位)、16(4位)、256(8位)、65536(16位)和1670万(24位)种颜色。

那么当Bitmap中的像素越多,每个点可表达的颜色越多,那么这个图片就越清晰、颜色越丰富。

Android中实例化Bitmap时可选择的质量类型:

只保存透明度信息,没有颜色信息

保存红绿蓝信息,没有透明度

透明度和红绿蓝都有,但是能使用的色彩数少

透明度和红绿蓝都有,但是能使用的色彩数较多

透明度和红绿蓝都有,但是能使用的色彩数多

Bitmap使用内存的计算

计算公式(针对内存中的Bitmap):

使用内存 = 横向像素数 * 竖向像素数 * 每个像素字节数

例如,对于像素数为1024 * 1024、质量为ARGB_8888的Bitmap来说,要将其加载到内存中,需要的内存为:

1024 * 1024 * 4 = 4MB

在Android应用中加载Bitmap比较复杂,原因有多种:

高效加载大图

既然Bitmap是实打实的图片数据,占用内存巨大,那么在某些场景必须加载大图时(如加载相册中的高清大图),应该如何处理呢?

可以按照如下几个步骤,高效的加载大图:

    val options = BitmapFactory.Options().apply {
        inJustDecodeBounds = true
    }
    BitmapFactory.decodeResource(resources, R.id.myimage, options)
    val imageHeight: Int = options.outHeight
    val imageWidth: Int = options.outWidth
    val imageType: String = options.outMimeType

例如,分辨率为 2048*1536 且以 4 作为 inSampleSize 进行解码的图片会生成大约 (2048/4)512 *(1536/4)384 的位图。将此图片加载到内存中需使用 0.75MB,而不是完整图片所需的 12MB(假设位图配置为 ARGB_8888)。

inSampleSize的数值,应为2的幂。即1、2、4、8...

那么,接下来要做的就是根据位图实际尺寸和实际需要尺寸,来计算实际的采用率:

  fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
      // 图像的原始高度和宽度
      val (height: Int, width: Int) = options.run { outHeight to outWidth }
      var inSampleSize = 1

      if (height > reqHeight || width > reqWidth) {

          val halfHeight: Int = height / 2
          val halfWidth: Int = width / 2

          // 计算最大的inSampleSize值,该值为2的幂,并且使height和width都大于请求的height和width。
          while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
              inSampleSize *= 2
          }
      }

      return inSampleSize

综上,加载大图时,整体流程是这样的:

fun decodeSampledBitmapFromResource(
           res: Resources,
           resId: Int,
           reqWidth: Int,
           reqHeight: Int
   ): Bitmap {
       // 先获取图片尺寸信息
       return BitmapFactory.Options().run {
           inJustDecodeBounds = true
           BitmapFactory.decodeResource(res, resId, this)

           // 计算 inSampleSize
           inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

           // 根据计算的采样率,最终加载实际的位图
           inJustDecodeBounds = false

           BitmapFactory.decodeResource(res, resId, this)
       }
   }

缓存位图

将单个位图加载到界面中非常简单,但如果需要同时加载较多的图片,情况就会变得复杂。在很多情况下(比如ListView、GridView或ViewPager等),屏幕上的图片与可能很快会滚动到屏幕上的图片加起来,数量是无限的。

对于这类组件,系统会通过循环利用移出屏幕的子视图来限制其对内存的占用。垃圾回收器也会释放已加载的位图,但当用户又滑回之前被回收的条目时,可以通过内存和磁盘缓存,让组件可以快速重新加载经过处理的图片。

配置LruCahce初始化相关参数

private lateinit var memoryCache: LruCache<String, Bitmap>

   override fun onCreate(savedInstanceState: Bundle?) {
       ...
       // 获取最大可用VM内存(KB单位),超过此数量将抛出OutOfMemory异常。 
       val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()

       // 将可用内存的1/8用作此内存缓存。
       val cacheSize = maxMemory / 8

       memoryCache = object : LruCache<String, Bitmap>(cacheSize) {

           override fun sizeOf(key: String, bitmap: Bitmap): Int {
               // 缓存大小单位是KB
               return bitmap.byteCount / 1024
           }
       }
       ...
   }

使用内存缓存:

fun loadBitmap(resId: Int, imageView: ImageView) {
       val imageKey: String = resId.toString()
       //内存缓存中有,那么直接用
       val bitmap: Bitmap? = getBitmapFromMemCache(imageKey)?.also {
           mImageView.setImageBitmap(it)
       } ?: run {
       //内存缓存中没有,那么异步加载
           mImageView.setImageResource(R.drawable.image_placeholder)
           val task = BitmapWorkerTask()
           task.execute(resId)
           null
       }
   }

异步加载图片时,需要及时的将位图存入内存缓存:

private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
       ...
       // 异步加载位图,加载成功后,及时将位图放入内存缓存
       override fun doInBackground(vararg params: Int?): Bitmap? {
           return params[0]?.let { imageId ->
               decodeSampledBitmapFromResource(resources, imageId, 100, 100)?.also { bitmap ->
                   addBitmapToMemoryCache(imageId.toString(), bitmap)
               }
           }
       }
       ...
   }

内存缓存有助于加快对最近查看过的位图的访问,但不能依赖于此缓存中保留的图片。GridView 这样拥有较大数据集的组件很容易将内存缓存填满。应用可能被其他任务(如电话)中断,而在后台时,应用可能会被终止,而内存缓存则会销毁。

在这些情况下,可以使用磁盘缓存来保存经过处理的位图,并在图片已不在内存缓存中时帮助减少加载时间。当然,从磁盘获取图片比从内存中加载缓慢,而且应该在后台线程中完成,因为磁盘读取时间不可预测。

完整的图片存取方式:

private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB的磁盘缓存空间
   private const val DISK_CACHE_SUBDIR = "thumbnails"
   ...
   private var diskLruCache: DiskLruCache? = null
   private val diskCacheLock = ReentrantLock()
   //即使是初始化磁盘缓存也需要执行磁盘操作,因此不应在主线程上执行。
   //不过,这也意味着可能会在初始化之前访问该缓存。
   //为了解决此问题,利用了一个 lock 对象来确保应用在磁盘缓存初始化之前不会从该缓存中读取数据。
   private val diskCacheLockCondition: Condition = diskCacheLock.newCondition()
   private var diskCacheStarting = true

   override fun onCreate(savedInstanceState: Bundle?) {
       ...
       // 初始化内存缓存
       ...
       // 异步初始化磁盘缓存
       val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR)
       InitDiskCacheTask().execute(cacheDir)
       ...
   }

   internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() {
       override fun doInBackground(vararg params: File): Void? {
           diskCacheLock.withLock {
               val cacheDir = params[0]
               diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE)
               diskCacheStarting = false // 完成初始化
               diskCacheLockCondition.signalAll() // 唤醒等待的线程
           }
           return null
       }
   }

   internal inner class  BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
       ...

       // 异步解码图像
       override fun doInBackground(vararg params: Int?): Bitmap? {
           val imageKey = params[0].toString()

           // 异步检查硬盘缓存
           return getBitmapFromDiskCache(imageKey) ?:
                   // 硬盘缓存中未找到
                   decodeSampledBitmapFromResource(resources, params[0], 100, 100)
                           ?.also {
                               // 将最终位图添加到缓存
                               addBitmapToCache(imageKey, it)
                           }
       }
   }

   fun addBitmapToCache(key: String, bitmap: Bitmap) {
       // 校验内存缓存中是否有可用缓存,没有则放入内存缓存
       if (getBitmapFromMemCache(key) == null) {
           memoryCache.put(key, bitmap)
       }

       // 同样放入磁盘缓存
       synchronized(diskCacheLock) {
           diskLruCache?.apply {
               if (!containsKey(key)) {
                   put(key, bitmap)
               }
           }
       }
   }

   fun getBitmapFromDiskCache(key: String): Bitmap? =
           diskCacheLock.withLock {
               while (diskCacheStarting) {
                   try {
                       diskCacheLockCondition.await()
                   } catch (e: InterruptedException) {
                   }

               }
               return diskLruCache?.get(key)
           }

   // 创建指定的应用程序缓存目录的唯一子目录。尝试在外部使用,但如果未安装,则退回到内部存储。
   fun getDiskCacheDir(context: Context, uniqueName: String): File {
       // 检查是否安装了介质或内置了存储,如果是,尝试使用外部缓存目录,否则使用内部缓存目录
       val cachePath =
               if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
                       || !isExternalStorageRemovable()) {
                   context.externalCacheDir.path
               } else {
                   context.cacheDir.path
               }

       return File(cachePath + File.separator + uniqueName)
   }

管理Bitmap内存

对于不同的Android版本,位图内存管理发生了如下的变更:

因此在不同的Android版本中,应采用不同的管理方案:

关于BitmapFactory.Options

这个参数的作用非常大,它可以设置Bitmap的采样率,通过改变图片的宽度、高度、缩放比例等,以达到减少图片的像素的目的。总的来说,通过设置这个值,可以更好地控制、显示,使用位图。

以下是其个属性及其含义:

属性 类型 含义
inJustDecodeBounds boolean 是否只解析图片信息
inSampleSize int 采样率(每隔多少个样本采样一次作为结果,比如4,代表没4个像素取1个作为结果返回,宽高都变为原来的1/4,总体为原来的1/16)
inScaled boolean 在需要缩放时,是否对当前文件进行缩放。false则不进行缩放;true或不设置,则会根据文件夹分辨率和屏幕分辨率动态缩放
inDensity int 设置文件所在资源文件夹的屏幕分辨率
inTargetDensity int 表示真实显示的屏幕分辨率,缩放比 = inTargetDensity/inDensity
inScreenDensity int 正在使用的实际屏幕的像素密度,目前没什么用
inPreferredConfig enum 设置像素的存储格式。RGB_565,ARGB_8888等
inMutable boolean 如果设置true,则解码方法将始终返回可变(可以修改像素信息)的位图,而不是不变(不可修改)的位图。
inBitmap Bitmap 重用此Bitmap,需要此Bitmap是可修改
outConfig Config 如果知道,解码位图将具有的配置
outHeight int 位图的最终高度
outWidth int 位图的最终宽度
inDither boolean 是否抖动,如果设置true,解码器将尝试抖动解码图像。例如图片原本是100px200px,而实际需要150px300px,设置此参数后,会将原来的100像素平铺,多出来的空白利用相邻两个颜色生成“中间色”来过渡。

关于Bitmap的可变和不可变

对于可变的Bitmap来说,通过setPixel(int x,int y,int color)等函数可以设置其中的像素值,而不可变的Bitmap使用这些方法就会报错。

那么什么情况下生成的Bitmap可变,什么时候不可变呢?答案就是:通过BitmapFactory加载的Bitmap都是不可变的;只有Bitmap中的几个函数创建的Bitmap才是像素可变的。这几个函数是:

1.copy(Config config, boolean isMutable)//isMutable传入true
2.createBitmap(@Nullable DisplayMetrics display, int width, int height,
        @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace)
3.createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,
        boolean filter)
上一篇下一篇

猜你喜欢

热点阅读