聊天气泡图片的动态拉伸、适配与镜像

  • 前情提要
    • 创建.9.png格式的图片
    • 从资源文件夹加载.9.png图片
    • 从本地文件加载“.9.png”图片
    • 项目痛点
  • 进阶探索
    • iOS中的方式
    • Android中的探索
      • 构造chunk数据
      • 构造padding数据
      • 镜像翻转功能
      • 屏幕的适配
      • 简单封装
      • 演示示例
        • 一条线段控制的拉伸
        • 两条线段控制的拉伸
        • padding的示例
        • 屏幕适配的示例
  • 效果一览
  • 参考文章

前情提要

春节又到了,作为一款丰富的社交类应用,免不了要上线几款和新年主题相关的聊天气泡背景。这不,可爱的兔兔和财神爷等等都安排上了,可是Android的气泡图上线流程等我了解后着实感觉有些许复杂,对比隔壁的iOS真是被吊打,且听我从头到尾细细详解一遍。

创建.9.png格式的图片


在开发上图所示的功能中,我们一般都会使用 .9.png 图片,那么一张普通png格式的图片怎么处理成 .9.png 格式呢,一起来简单回顾下。

在Android Studio中,对一张普通png图片右键,然后点击 “Create 9-Patch file…”,选择新图片保存的位置后,双击新图就会显示图片编辑器,图片左侧的黑色线段可以控制图片的竖向拉伸区域,上侧的黑色线段可以控制图片的横向拉伸区域,下侧和右侧的黑色线段则可以控制内容的填充区域,编辑后如下图所示:

上图呢是居中拉伸的情况,但是如果中间有不可拉伸元素的话如何处理呢(一般情况下我们也不会有这样的聊天气泡,这里是拜托UI小姐姐专门修改图片做的示例),如下图所示,这时候拉伸的话左侧和上侧就需要使用两条(多条)线段来控制拉伸的区域了,从而避免中间的财神爷被拉伸:

OK,.9.png格式图片的处理就是这样了。

从资源文件夹加载.9.png图片

比如加载drawable或者mipmap资源文件夹中的图片,这种加载方式的话很简单,直接给文字设置背景就可以了,刚刚处理过的小兔子图片放在drawable-xxhdpi文件夹下,命名为rabbit.9.png,示例代码如下所示:

textView.background = ContextCompat.getDrawable(this, R.drawable.rabbit)

从本地文件加载“.9.png”图片

如果我们将上述rabbit.9.png图片直接放到应用缓存文件夹中,然后通过bitmap进行加载,伪代码如下:

textView.text = "直接加载本地.9.png图片"
textView.background =BitmapDrawable.createFromPath(cacheDir.absolutePath + File.separator + "rabbit.9.png")

则显示效果如下:

可以看到,这样是达不到我们想要的效果的,整张图片被直接进行拉伸了,完全没有我们上文设计的拉伸效果。

其实要想达到上文设计的居中拉伸效果,我们需要使用aapt工具对.9.png图片再进行下处理(在Windows系统上aapt工具所在位置为:你SDK目录\build-tools\版本号\aapt.exe),Windows下的命令如下所示:

.\aapt.exe s -i .\rabbit.9.png -o rabbit9.png

将处理过后新生成的rabbit9.png图片放入到应用缓存文件夹中,然后通过bitmap直接进行加载,代码如下:

textView.text = "加载经aapt处理过的本地图片"
textView.background =BitmapDrawable.createFromPath(cacheDir.absolutePath + File.separator + "rabbit9.png")

则显示效果正常,如下所示:

也就是说如果我们需要从本地或者assets文件夹中加载可拉伸图片的话,那么整个处理的流程就是:根据源rabit.png图片创建rabbit.9.png图片 -> 使用aapt处理生成新的rabbit9.png图片。

项目痛点

所以,以上就是目前项目中的痛点,每次增加一个聊天气泡背景,Android组都需要从UI小姐姐那里拿两张图片,一左一右,然后分别处理成 .9.png 图,然后还需要用aapt工具处理,然后再上传到服务器。后台还需要针对Android和iOS平台下发不同的图片,这也太复杂了。
所以我们的目标就是只需要一张通用的气泡背景图,直接上传服务器,移动端下载下来后,在本地做 拉伸镜像、缩放等 功能的处理,那么一起来探索下吧。

进阶探索

我们来先对比看下iOS的处理方式,然后升级我们的项目。

iOS中的方式

只需要一个原始的png的图片即可,人家有专门的resizableImage函数来处理拉伸,大致的示例代码如下所示:

let image : UIImage = UIImage(named: "rabbit.png")
image.resizableImage(withCapInsets: .init(top: 20, left: 20, right:20, bottom:20))

注意:这里的withCapInsets参数的含义应该是等同与Android中的padding。padding的区域就是被保护不会拉伸的区域,而剩下的区域则会被拉伸来填充。
可以看到这里其实是有一定的约束规范的,UI小姐姐是按照此规范来进行气泡图的设计的,所以我们也可以遵循大致的约束,和iOS使用同一张气泡背景图片即可。

Android中的探索

那么在Android中有没有可能也直接通过代码来处理图片的拉伸呢?也可以有!!!
原理请参考《Android动态布局入门及NinePatchChunk解密》,各种思想的碰撞请参考《Create a NinePatch/NinePatchDrawable in runtime》。
站在前面巨人的肩膀上看,最终我们需要自定义创建的就是一个NinePatchDrawable对象,这样可以直接设置给TextView的background属性或者其他drawable属性。那么先来看下创建该对象所需的参数吧:

/**
* Create drawable from raw nine-patch data, setting initial target density
* based on the display metrics of the resources.
*/
public NinePatchDrawable(Resources res,Bitmap bitmap,byte[] chunk,Rect padding,String srcName
)

主要就是其中的两个参数:

  • byte[] chunk:构造chunk数据,是构造可拉伸图片的数据结构
  • Rect padding:padding数据,同xml中的padding含义,不要被Rect所迷惑

构造chunk数据

这里构造数据可是有说法的,我们先以上文兔子图片的拉伸做示例,在该示例中,横向和竖向都分别有一条线段来控制拉伸,那么我们定义如下:
横向线段的起点位置的百分比为patchHorizontalStart,终点位置的百分比为patchHorizontalEnd;
竖向线段的起点位置的百分比为patchVerticalStart,终点位置的百分比为patchVerticalEnd;
width和height分别为传入进来的bitmap的宽度和高度,示例代码如下:

private fun buildChunk(): ByteArray {// 横向和竖向都只有一条线段,一条线段有两个端点val horizontalEndpointsSize = 2val verticalEndpointsSize = 2val NO_COLOR = 0x00000001val COLOR_SIZE = 9 //could change, may be 2 or 6 or 15 - but has no effect on outputval arraySize = 1 + 2 + 4 + 1 + horizontalEndpointsSize + verticalEndpointsSize + COLOR_SIZEval byteBuffer = ByteBuffer.allocate(arraySize * 4).order(ByteOrder.nativeOrder())byteBuffer.put(1.toByte()) //was translatedbyteBuffer.put(horizontalEndpointsSize.toByte()) //divisions xbyteBuffer.put(verticalEndpointsSize.toByte()) //divisions ybyteBuffer.put(COLOR_SIZE.toByte()) //color size// skipbyteBuffer.putInt(0)byteBuffer.putInt(0)// padding 设为0,即使设置了数据,padding依旧可能不生效byteBuffer.putInt(0)byteBuffer.putInt(0)byteBuffer.putInt(0)byteBuffer.putInt(0)// skipbyteBuffer.putInt(0)// regions 控制横向拉伸的线段数据val patchLeft = (width * patchHorizontalStart).toInt()val patchRight = (width * patchHorizontalEnd).toInt()byteBuffer.putInt(patchLeft)byteBuffer.putInt(patchRight)// regions 控制竖向拉伸的线段数据val patchTop = (height * patchVerticalStart).toInt()val patchBottom = (height * patchVerticalEnd).toInt()byteBuffer.putInt(patchTop)byteBuffer.putInt(patchBottom)for (i in 0 until COLOR_SIZE) {byteBuffer.putInt(NO_COLOR)}return byteBuffer.array()
}

OK,上面是横向竖向都有一条线段来控制图片拉伸的情况,再看上文财神爷图片的拉伸示例,就分别都是两条线段控制了,也有可能需要更多条线段来控制,所以我们需要稍微改造下我们的代码,首先定义一个PatchRegionBean的实体类,该类定义了一条线段的起点和终点(都是百分比):

data class PatchRegionBean(val start: Float,val end: Float
)

在类中定义横向和竖向竖向线段的列表,用来存储这些数据,然后改造buildChunk()方法如下:

private var patchRegionHorizontal = mutableListOf<PatchRegionBean>()
private var patchRegionVertical = mutableListOf<PatchRegionBean>()private fun buildChunk(): ByteArray {// 横向和竖向端点的数量 = 线段数量 * 2val horizontalEndpointsSize = patchRegionHorizontal.size * 2val verticalEndpointsSize = patchRegionVertical.size * 2val NO_COLOR = 0x00000001val COLOR_SIZE = 9 //could change, may be 2 or 6 or 15 - but has no effect on outputval arraySize = 1 + 2 + 4 + 1 + horizontalEndpointsSize + verticalEndpointsSize + COLOR_SIZEval byteBuffer = ByteBuffer.allocate(arraySize * 4).order(ByteOrder.nativeOrder())byteBuffer.put(1.toByte()) //was translatedbyteBuffer.put(horizontalEndpointsSize.toByte()) //divisions xbyteBuffer.put(verticalEndpointsSize.toByte()) //divisions ybyteBuffer.put(COLOR_SIZE.toByte()) //color size// skipbyteBuffer.putInt(0)byteBuffer.putInt(0)// padding 设为0,即使设置了数据,padding依旧可能不生效byteBuffer.putInt(0)byteBuffer.putInt(0)byteBuffer.putInt(0)byteBuffer.putInt(0)// skipbyteBuffer.putInt(0)// regions 控制横向拉伸的线段数据patchRegionHorizontal.forEach {byteBuffer.putInt((width * it.start).toInt())byteBuffer.putInt((width * it.end).toInt())}// regions 控制竖向拉伸的线段数据patchRegionVertical.forEach {byteBuffer.putInt((height * it.start).toInt())byteBuffer.putInt((height * it.end).toInt())}for (i in 0 until COLOR_SIZE) {byteBuffer.putInt(NO_COLOR)}return byteBuffer.array()
}

构造padding数据

对比刚刚的chunk数据,padding就显得尤其简单了,注意这里传递来的值依旧是百分比,而且需要注意别和Rect的含义搞混了即可:

fun setPadding(paddingLeft: Float,paddingRight: Float,paddingTop: Float,paddingBottom: Float,
): NinePatchDrawableBuilder {this.paddingLeft = paddingLeftthis.paddingRight = paddingRightthis.paddingTop = paddingTopthis.paddingBottom = paddingBottomreturn this
}/*** 控制内容填充的区域* (注意:这里的left,top,right,bottom同xml文件中的padding意思一致,只不过这里是百分比形式)*/
private fun buildPadding(): Rect {val rect = Rect()rect.left = (width * paddingLeft).toInt()rect.right = (width * paddingRight).toInt()rect.top = (height * paddingTop).toInt()rect.bottom = (height * paddingBottom).toInt()return rect
}

镜像翻转功能

因为是聊天气泡背景,所以一般都会有左右两个位置的展示,而这俩文件一般情况下都是横向镜像显示的,在Android中好像也没有直接的图片镜像功能,但好在之前做海外项目LTR以及RTL时候了解到一个投机取巧的方式,通过设置scale属性为-1来实现。这里我们同样可以这么做,因为最终处理的都是bitmap图片,示例代码如下:

/*** 构造bitmap信息* 注意:需要判断是否需要做横向的镜像处理*/
private fun buildBitmap(): Bitmap? {return if (!horizontalMirror) {bitmap} else {bitmap?.let {val matrix = Matrix()matrix.setScale(-1f, 1f)val newBitmap = Bitmap.createBitmap(it,0, 0, it.width, it.height,matrix, true)it.recycle()newBitmap}}
}

如果需要镜像处理我们就通过设置Matrix的scaleX的属性为-1f,这就可以做到横向镜像的效果,竖向则保持不变,然后通过Bitmap类创建新的bitmap即可。
图像镜像反转的情况下,还需要注意的两点是:

  • chunk的数据中横向内容需要重新处理
  • padding的数据中横向内容需要重新处理
/*** chunk数据的修改*/
if (horizontalMirror) {patchRegionHorizontal.forEach {byteBuffer.putInt((width * (1f - it.end)).toInt())byteBuffer.putInt((width * (1f - it.start)).toInt())}
} else {patchRegionHorizontal.forEach {byteBuffer.putInt((width * it.start).toInt())byteBuffer.putInt((width * it.end).toInt())}
}/*** padding数据的修改*/
if (horizontalMirror) {rect.left = (width * paddingRight).toInt()rect.right = (width * paddingLeft).toInt()
} else {rect.left = (width * paddingLeft).toInt()rect.right = (width * paddingRight).toInt()
}

屏幕的适配

屏幕适配的话其实就是利用Bitmap的density属性,如果UI给定的图是按照480dpi设计的,那么就设置为480dpi或者相近的dpi即可:

// 注意:是densityDpi的值,320、480、640等
bitmap.density = 480

简单封装

通过上述两步重要的过程我们已经知道如何构造所需的chunk和padding数据了,那么简单封装一个类来处理吧,加载的图片我们可以通过资源文件夹(drawable、mipmap),asstes文件夹,手机本地文件夹来获取,所以对上述三种类型都做下支持:

/*** 设置资源文件夹中的图片*/
fun setResourceData(resources: Resources,resId: Int,horizontalMirror: Boolean = false
): NinePatchDrawableBuilder {val bitmap: Bitmap? = try {BitmapFactory.decodeResource(resources, resId)} catch (e: Throwable) {e.printStackTrace()null}return setBitmapData(bitmap = bitmap,resources = resources,horizontalMirror = horizontalMirror)
}/*** 设置本地文件夹中的图片*/
fun setFileData(resources: Resources,file: File,horizontalMirror: Boolean = false
): NinePatchDrawableBuilder {val bitmap: Bitmap? = try {BitmapFactory.decodeFile(file.absolutePath)} catch (e: Throwable) {e.printStackTrace()null}return setBitmapData(bitmap = bitmap,resources = resources,horizontalMirror = horizontalMirror)
}/*** 设置assets文件夹中的图片*/
fun setAssetsData(resources: Resources,assetFilePath: String,horizontalMirror: Boolean = false
): NinePatchDrawableBuilder {var bitmap: Bitmap?try {val inputStream = resources.assets.open(assetFilePath)bitmap = BitmapFactory.decodeStream(inputStream)inputStream.close()} catch (e: Throwable) {e.printStackTrace()bitmap = null}return setBitmapData(bitmap = bitmap,resources = resources,horizontalMirror = horizontalMirror)
}/*** 直接处理bitmap数据*/
fun setBitmapData(bitmap: Bitmap?,resources: Resources,horizontalMirror: Boolean = false
): NinePatchDrawableBuilder {this.bitmap = bitmapthis.width = bitmap?.width ?: 0this.height = bitmap?.height ?: 0this.resources = resourcesthis.horizontalMirror = horizontalMirrorreturn this
}

横向和竖向的线段需要支持多段,所以分别使用两个列表来进行管理:

fun setPatchHorizontal(vararg patchRegion: PatchRegionBean): NinePatchDrawableBuilder {patchRegion.forEach {patchRegionHorizontal.add(it)}return this
}fun setPatchVertical(vararg patchRegion: PatchRegionBean): NinePatchDrawableBuilder {patchRegion.forEach {patchRegionVertical.add(it)}return this
}

演示示例

我们使用一个5x5的25宫格图片来进行演示,这样我们可以很方便的看出来拉伸或者边距的设置到底有没有生效,将该图片放入资源文件夹中,页面上创建一个展示该图片用的ImageView,假设图片大小是200x200,然后创建一个TextView,通过我们自己的可拉伸功能设置文字的背景。
(注:演示所用的图片是请UI小哥哥帮忙处理的,听完说完我的需求后,UI小哥哥二话没说当着我的面直接出了十来种颜色风格的图片让我选,相当给力!!!)

一条线段控制的拉伸

示例代码如下:

textView.width = 800
textView.background = NinePatchDrawableBuilder().setResourceData(resources = resources,resId = R.drawable.sample_1,horizontalMirror = false).setPatchHorizontal(PatchRegionBean(start = 0.4f, end = 0.6f),).build()

显示效果如下:

可以看到竖向上没有拉伸,横向上图片 0.4-0.6 的区域全部被拉伸,然后填充了800的宽度。

两条线段控制的拉伸

接下来再看这段代码示例,这里我们横向上添加了两条线段,分别是从0.2-0.4,0.6-0.8:

textView.width = 800
textView.background = NinePatchDrawableBuilder().setResourceData(resources = resources,resId = R.drawable.sample_1,horizontalMirror = false).setPatchHorizontal(PatchRegionBean(start = 0.2f, end = 0.4f),PatchRegionBean(start = 0.6f, end = 0.8f),).build()

显示效果如下:

可以看到横向上中间的(0.4-0.6)的部分没有被拉伸,(0.2-0.4)以及(0.6-0.8)的部分被分别拉伸,然后填充了800的宽度。

padding的示例

我们添加上文字,并且结合padding来进行演示下,这里先设置padding距离边界都为0.2的百分比,示例代码如下:

textView.background = NinePatchDrawableBuilder().setResourceData(resources = resources,resId = R.drawable.sample_2,horizontalMirror = false).setPatchHorizontal(PatchRegionBean(start = 0.4f, end = 0.6f),).setPatchVertical(PatchRegionBean(start = 0.4f, end = 0.6f),).setPadding(paddingLeft = 0.2f,paddingRight = 0.2f,paddingTop = 0.2f,paddingBottom = 0.2f).build()

显示效果如下:

然后将padding的边距都改为0.4的百分比,显示效果如下:

屏幕适配的示例

上述的图片都是在480dpi下显示的,这里我们将densityDpi设置为960,按道理来说拉伸图展示会小一倍,如下图所示:

textView.background = NinePatchDrawableBuilder().......setDensityDpi(densityDpi = 960).build()

效果一览

整个工具类实现完毕后,又简单写了两个页面通过设置各种参数来实时预览图片拉伸和镜像以及padding的情况,效果展示如下:

整体的探索过程到此基本就结束了,效果是实现了,然而性能和兼容性还无法保证,接下来需要进一步做下测试才能上线。可能有大佬很早就接触过这些功能,如果能指点下,鄙人则不胜感激。

文中若有纰漏之处还请大家多多指教。

参考文章

  1. Android 点九图机制讲解及在聊天气泡中的应用
  2. Android动态布局入门及NinePatchChunk解密
  3. Android点九图总结以及在聊天气泡中的使用

聊天气泡图片的动态拉伸、适配与镜像相关推荐

  1. UIImageView 显示不规则图片,类似微信聊天气泡图片展示效果

    微信聊天气泡,图片充满显示,效果: -(void)layoutSubviews { [super layoutSubviews]; CAShapeLayer * _maskLayer = [CASha ...

  2. android 仿微信聊天气泡显示图片,仿微信聊天气泡 图片尖角 按下变暗

    实现微信气泡图片尖角 //-------------gen corner bitmap flow------------------------ //load the bg: .9.png which ...

  3. 防微信聊天气泡图片实现

    先看下效果图 防微信实现如图的 图片显示效果. 接上篇博客介绍的图形图片的实现 , 这里通过BitmapSharder来实现这个效果. 主要麻烦的地方就是画出气泡形状的path. 这里设置自定义的属性 ...

  4. android气泡组件,Android 聊天气泡

    网上搜到的只有一篇是自定义的TextView,其使用比较麻烦,所以采用大众化的方法--使用9.png来实现. 这里主要介绍sdk tool的draw9patch.bat的使用. 这个bat执行文件打开 ...

  5. 微信聊天气泡随意换,一键制作超级简单,让聊天不再枯燥!

    除了QQ微信也可以制作聊天气泡,今天小编给大家分享一款微信小程序,只需简单几步就可以制作出个性十足的聊天气泡效果了,赶快一起来看看吧! 这款小程序中提供了丰富的气泡模板,其中包括游戏.明星以及动漫模块 ...

  6. android 聊天气泡背景图片,聊天气泡背景图片拉伸设置

    以前一直对类似聊天气泡背景图片拉伸的设置纠结,不管如何设置UIEdgeInsets属性都不能正常设置,今天对以下几种情况进行了总结,如有需要的同学可进行参考: 说明: /** 1.UIEdgeInse ...

  7. Android聊天气泡如何使用网络.png图片实现拉伸

    最近在做的是一个语音连麦项目,里面有公屏聊天,之前的聊天背景是自己shape画的纯色或者渐变色,但是新需求是让用上UI妹子设计的各种各样气泡比如这样 然后拉伸的,这种图片是不放在本地的,要后台返回链接 ...

  8. android .9图片在代码中的设置,Android点九图总结以及在聊天气泡中的使用

    编辑推荐: 本文来自于腾讯云,介绍了点九图的本质,聊天气泡中使用点九图,其它问题等. 1. 点九图介绍 这一块是对点九图的简单介绍,如果对这块已经有了解的话,可以直接跳到2,看看聊天气泡中如何使用点九 ...

  9. android 图片气泡,关于实现微信聊天气泡里显示图片解决方案

    关于实现微信聊天气泡里显示图片 这是微信的效果,气泡中的图片没有边距 这是我的效果,背景气泡是用.9.png图片组成的一个selector,气泡中的图片有边距  如何才能像微信那样没有边矩呢? --- ...

最新文章

  1. 报错解决:error: this statement may fall through [-Werror=implicit-fallthrough=]
  2. 英语和汉语谁更高效?17大主流语言测试结果:没有优劣之分,带宽都是每秒39bit...
  3. 109. Leetcode 309. 最佳买卖股票时机含冷冻期 (动态规划-股票交易)
  4. nyoj-Color the necklace(Ploya定理 + 欧拉函数 + 扩展欧几里得(求逆元))
  5. 最长平台(信息学奥赛一本通-T1116)
  6. C++ 用遗传算法解决TSP问题,旅行商问题
  7. php赋值一个数组,PHP入门教程之数组的定义和赋值
  8. 【二分图】洛谷P2055假期的宿舍
  9. Recoverit for Mac专业的数据恢复工具
  10. mongoddb常用增删改查命令--推荐查询命令:
  11. 拓端tecdat|R语言rjags使用随机效应进行臭氧数据分析
  12. Java基础(四)线程快速了解
  13. CDH-CM资源下载!!免费!!!
  14. 微信最新版下载 8.0.6
  15. Dirt Ratio HDU - 6070
  16. Linux之无人值守安装系统
  17. 什么是 5G CPE
  18. WordPress码支付支付插件 WordPress付费阅读查看插件 付费下载插件
  19. UE4:按键按下触发声音事件,离开位置声音停止
  20. [css]版心和布局流程

热门文章

  1. 谷歌浏览器插件打包ChromePackage-extention
  2. 资深程序员雷总对代码的执念
  3. java do po dto_彻底搞懂DAO,PO,BO,DTO,VO,DO
  4. 数字中国城市巡礼之开封:千年古都的智慧新生
  5. 图章制作软件测试自学,SecSeal安全电子印章系统内部测试大纲.doc
  6. 工业物联网(IIoT)生态构建三字经:先做人、后修心、再打怪!
  7. 拼多多上架工具有哪些,拼多多产品上架流程详解
  8. 车载之ECU、VCU、MCU、HCU
  9. 【Linux】解决Centos软件安装源出错
  10. 魔兽世界mysql闪退_魔兽世界怀旧服闪退怎么办