需求是这样的

气泡

1. 点9图简单介绍

Android为了使用同一张图作为大小区域背景,设计了一种可以指定区域拉伸的图片格式“.9.png”,这种图片格式就是点九图。Android 特有的一种格式,在ios开发中,可以在代码中指定某个点进行拉伸。

点9图

如上图,点九图的本质实际上是在图片的四周各增加了1px的像素,并使用纯黑(#FF000000)的线进行标记,其它的与原图没有任何区别

标记位置 含义

左--> 纵向拉伸区域

上--> 横向拉伸区域

右--> 纵向显示区域

下--> 横向显示区域

点9图可以通过ps等p图工具手动画黑线、Draw9patch工具、AndroidStudio、或者

在线工具点击进入

2. 点9图的使用

Android中使用点九图,主要有三种形式,使用res文件夹中的点九图,使用assets文件夹中的点九图以及使用网上拉取的点九图。

注意

Android并不是直接使用点九图,而是在编译时将其转换为另外一种格式,这种格式是将其四周的黑色像素保存至Bitmap类中的一个名为mNinePatchChunk的byte[]中,并抹除掉四周的这一个像素的宽度;接着在使用时,如果Bitmap的这个mNinePatchChunk不为空,且为9patch chunk,则将其构造为NinePatchDrawable,否则将会被构造为BitmapDrawable,最终设置给view,NinePatchDrawable的拉伸主要是通过其draw方法实现的。

总而言之,最后打出的包中的点九图,已经不是原来的带黑线的点九图了。而是通过appt工具,把点9图的黑线信息编码到png的图片字节中。

3. 把点9图应用到气泡中

点9图放到res文件夹, 在编译期已经已经把点9信息合成到png中, 我们使用的时候,系统自动解析信息,所以使用起来基本无感知,但是放到文件和网络中的点9图片不能直接使用。如果直接加载就会出现如下效果

示例图片

黑线仍然存在,且没有固定区域拉伸的效果

4. 解决思路

既然无法直接使用点9图,就要寻求系统是如何使用点9图的,包括他的编译过程,也就是点9信息存储和使用方式。

关于点9图的源码分析,参考腾讯音乐团队的blog,详见文末的参考文献

根据之前的讨论我们知道,画黑线的点九图与普通图片的区别主要在于四周多了1px的黑线,而转换后的点九图则没有这1px的黑线,但是它却包含了用于拉伸的信息。所以我们要从这个信息里面入手。

第一种方案:直接上传点9图片,下载之后再做处理。UI生成端比较方便,但是处理过程比较繁琐, 而且资源图片要和iOS进行区分,后台配置比较繁琐。

第二种方案:设计生成点9图之后,通过appt命令,把点9信息编码到在图片资源中,Android 端获取图片后直接解析点9信息。优点是可以和iOS端共用一个资源图,解析也比较方便,缺点是上传前需要提前进行appt命令合成,而且有合成错误的风险,。

第三种方案:直接和iOS使用同一套资源,Android手动添加patch点,便于适配不使用padding。优点是配置端比较方便,缺点是解析和生成比较繁琐。

第一种方案就不说了,三端都繁琐的事情,就没必要做了。

第二种方案处理流程

a> UI端生成点9图片

b> 通过命令 aapt s -i xx.9.png -o xx.png 生成包含点9信息的图片,上传服务器

c> Android下载之后,解析点9信息

var bmp = Bitmap.createBitmap(bmpTemp)

var chunk = bmp?.ninePatchChunk

d> 判断点9信息正确之后,生成NinePatchDrawable,设置对应view backgroud

if (NinePatch.isNinePatchChunk(chunk)) {

var ninePatchDrawable = NinePatchDrawable(context?.resources, bmp, chunk, NinePatchChunk.getPaddingRect(chunk), null)

view.post {

view.background = ninePatchDrawable

}

} else {

var bitmapDrawable = BitmapDrawable(context?.resources, bmp)

view.post {

view.background = bitmapDrawable

}

}

中间遇到的一个坑,就是这种方式加载的点9图无法使用padding效果,就是点9图的右下内容区域控制,查资料找到实现方案NinePatchDrawable的第三个参数padding内容, 如下类中的getPaddingRect方法。

import android.graphics.Rect;

import com.richard.base.BaseApplication;

import java.nio.ByteBuffer;

import java.nio.ByteOrder;

/**

* **************************************

* 项目名称:Giggle

*

* @Author wuzhiguo

* 创建时间: 2020/8/14 11:25 AM

* 用途:

* **************************************

*/

public class NinePatchChunk {

private static final String TAG = "NinePatchChunk";

public final Rect mPaddings = new Rect();

public int mDivX[];

public int mDivY[];

public int mColor[];

private static float density = BaseApplication.Companion.getInstance().getResources().getDisplayMetrics().density;

private static void readIntArray(final int[] data, final ByteBuffer buffer) {

for (int i = 0, n = data.length; i < n; ++i)

data[i] = buffer.getInt();

}

private static void checkDivCount(final int length) {

if (length == 0 || (length & 0x01) != 0)

throw new IllegalStateException("invalid nine-patch: " + length);

}

public static Rect getPaddingRect(final byte[] data) {

NinePatchChunk deserialize = deserialize(data);

if (deserialize == null) {

return new Rect();

}

return deserialize.mPaddings;

}

public static NinePatchChunk deserialize(final byte[] data) {

final ByteBuffer byteBuffer =

ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());

if (byteBuffer.get() == 0) {

return null; // is not serialized

}

final NinePatchChunk chunk = new NinePatchChunk();

chunk.mDivX = new int[byteBuffer.get()];

chunk.mDivY = new int[byteBuffer.get()];

chunk.mColor = new int[byteBuffer.get()];

try {

checkDivCount(chunk.mDivX.length);

checkDivCount(chunk.mDivY.length);

} catch (Exception e) {

return null;

}

// skip 8 bytes

byteBuffer.getInt();

byteBuffer.getInt();

chunk.mPaddings.left = byteBuffer.getInt();

chunk.mPaddings.right = byteBuffer.getInt();

chunk.mPaddings.top = byteBuffer.getInt();

chunk.mPaddings.bottom = byteBuffer.getInt();

// skip 4 bytes

byteBuffer.getInt();

readIntArray(chunk.mDivX, byteBuffer);

readIntArray(chunk.mDivY, byteBuffer);

readIntArray(chunk.mColor, byteBuffer);

return chunk;

}

}

另一个坑就是fresco图片加载框架通过ImagePipeline、BaseBitmapDataSubscriber获取的bitmap对象中,NinePatchChunk信息是空的,只能通过DataSource方式获取字节流转换成bitmap才可以。

第三种方案,手动添加拉伸信息到bitmap

要实现这种方案,首先要对Bitmap和NinePatch以及png的tunk信息有一定的了解,详情可参阅QQ音乐的blog讲解

实现方案如下

import android.content.res.Resources

import android.graphics.Bitmap

import android.graphics.NinePatch

import android.graphics.drawable.NinePatchDrawable

import java.nio.ByteBuffer

import java.nio.ByteOrder

/**

* **************************************

* 项目名称:Giggle

*

* @Author wuzhiguo

* 创建时间: 2020/8/14 11:25 AM

* 用途: 手动构建NinePatch

* **************************************

*/

class NinePatchBuilder {

var width: Int

var height: Int

var bitmap: Bitmap? = null

var resources: Resources? = null

private val xRegions = mutableListOf()

private val yRegions = mutableListOf()

constructor(resources: Resources?, bitmap: Bitmap) {

width = bitmap.width

height = bitmap.height

this.bitmap = bitmap

this.resources = resources

}

constructor(width: Int, height: Int) {

this.width = width

this.height = height

}

fun addXRegion(x: Int, width: Int): NinePatchBuilder {

xRegions.add(x)

xRegions.add(x + width)

return this

}

fun addXRegionPoints(x1: Int, x2: Int): NinePatchBuilder {

xRegions.add(x1)

xRegions.add(x2)

return this

}

fun addXRegion(xPercent: Float, widthPercent: Float): NinePatchBuilder {

val xtmp = (xPercent * width).toInt()

xRegions.add(xtmp)

xRegions.add(xtmp + (widthPercent * width).toInt())

return this

}

fun addXRegionPoints(x1Percent: Float, x2Percent: Float): NinePatchBuilder {

xRegions.add((x1Percent * width).toInt())

xRegions.add((x2Percent * width).toInt())

return this

}

fun addXCenteredRegion(width: Int): NinePatchBuilder {

val x = ((this.width - width) / 2)

xRegions.add(x)

xRegions.add(x + width)

return this

}

fun addXCenteredRegion(widthPercent: Float): NinePatchBuilder {

val width = (widthPercent * width).toInt()

val x = ((this.width - width) / 2)

xRegions.add(x)

xRegions.add(x + width)

return this

}

fun addYRegion(y: Int, height: Int): NinePatchBuilder {

yRegions.add(y)

yRegions.add(y + height)

return this

}

fun addYRegionPoints(y1: Int, y2: Int): NinePatchBuilder {

yRegions.add(y1)

yRegions.add(y2)

return this

}

fun addYRegion(yPercent: Float, heightPercent: Float): NinePatchBuilder {

val ytmp = (yPercent * height).toInt()

yRegions.add(ytmp)

yRegions.add(ytmp + (heightPercent * height).toInt())

return this

}

fun addYRegionPoints(y1Percent: Float, y2Percent: Float): NinePatchBuilder {

yRegions.add((y1Percent * height).toInt())

yRegions.add((y2Percent * height).toInt())

return this

}

fun addYCenteredRegion(height: Int): NinePatchBuilder {

val y = ((this.height - height) / 2)

yRegions.add(y)

yRegions.add(y + height)

return this

}

fun addYCenteredRegion(heightPercent: Float): NinePatchBuilder {

val height = (heightPercent * height).toInt()

val y = ((this.height - height) / 2)

yRegions.add(y)

yRegions.add(y + height)

return this

}

fun buildChunk(): ByteArray {

if (xRegions.size == 0) {

xRegions.add(0)

xRegions.add(width)

}

if (yRegions.size == 0) {

yRegions.add(0)

yRegions.add(height)

}

/* example code from a anwser above

// The 9 patch segment is not a solid color.

private static final int NO_COLOR = 0x00000001;

ByteBuffer buffer = ByteBuffer.allocate(56).order(ByteOrder.nativeOrder());

//was translated

buffer.put((byte)0x01);

//divx size

buffer.put((byte)0x02);

//divy size

buffer.put((byte)0x02);

//color size

buffer.put(( byte)0x02);

//skip

buffer.putInt(0);

buffer.putInt(0);

//padding

buffer.putInt(0);

buffer.putInt(0);

buffer.putInt(0);

buffer.putInt(0);

//skip 4 bytes

buffer.putInt(0);

buffer.putInt(left);

buffer.putInt(right);

buffer.putInt(top);

buffer.putInt(bottom);

buffer.putInt(NO_COLOR);

buffer.putInt(NO_COLOR);

return buffer;*/

val NO_COLOR = 1 //0x00000001;

val COLOR_SIZE = 9 //could change, may be 2 or 6 or 15 - but has no effect on output

val arraySize: Int = 1 + 2 + 4 + 1 + xRegions.size + yRegions.size + COLOR_SIZE

val byteBuffer: ByteBuffer =

ByteBuffer.allocate(arraySize * 4).order(ByteOrder.nativeOrder())

byteBuffer.put(1.toByte()) //was translated

byteBuffer.put(xRegions.size.toByte()) //divisions x

byteBuffer.put(yRegions.size.toByte()) //divisions y

byteBuffer.put(COLOR_SIZE.toByte()) //color size

//skip

byteBuffer.putInt(0)

byteBuffer.putInt(0)

//padding -- always 0 -- left right top bottom

byteBuffer.putInt(0)

byteBuffer.putInt(0)

byteBuffer.putInt(0)

byteBuffer.putInt(0)

//skip

byteBuffer.putInt(0)

for (rx in xRegions) byteBuffer.putInt(rx) // regions left right left right ...

for (ry in yRegions) byteBuffer.putInt(ry) // regions top bottom top bottom ...

for (i in 0 until COLOR_SIZE) byteBuffer.putInt(NO_COLOR)

return byteBuffer.array()

}

fun buildNinePatch(): NinePatch? {

val chunk = buildChunk()

return if (bitmap != null) NinePatch(bitmap, chunk, null) else null

}

fun build(): NinePatchDrawable? {

val ninePatch = buildNinePatch()

return ninePatch?.let { NinePatchDrawable(resources, it) }

}

}

简单使用方式如下

val builder = NinePatchBuilder(resources, bmpTemp)

builder.addXCenteredRegion(10)

builder.addYCenteredRegion(10)

val drawable = builder.build()

view.background = drawable

这种方式会保存原来的点9图padding信息,如果原图没有的话, 可以手动在view上做padding控制。

5. 注意事项和遇见的坑

一定要使用缓存,不然异步加载的过程中,在list中显示会有问题,跳变很严重,尤其是在快速滑动的时候, 最好能复用图片缓存框架,内存+磁盘两种缓存都要做。

代码操作bitmap一定要及时释放回收,最好能有一定的保障机制,避免bitmap大量占用内存。比如发现图片过大,可以走default处理方式。

给view设置Drawable背景的时候,会把view本身的padding删除,解决方案是提前把view的padding获取到,设置完drawable背景之后,再把padding设置上

public static void setBackgroundAndKeepPadding(View view, Drawable backgroundDrawable) {

Rect drawablePadding = new Rect();

backgroundDrawable.getPadding(drawablePadding);

int top = view.getPaddingTop() + drawablePadding.top;

int left = view.getPaddingLeft() + drawablePadding.left;

int right = view.getPaddingRight() + drawablePadding.right;

int bottom = view.getPaddingBottom() + drawablePadding.bottom;

view.setBackgroundDrawable(backgroundDrawable);

view.setPadding(left, top, right, bottom);

}

屏幕适配问题,网络下发的图可能尺寸对不上,要先适配屏幕,再做点9,再设置背景

/**

* 指定大小缩放, 为了屏幕适配

* @param bmpTemp Bitmap

* @param bubblePicHeight Float

* @return Bitmap

*/

private fun decodeBitmap(bmpTemp: Bitmap, bubblePicHeight: Float): Bitmap {

val width = bmpTemp.width

val height = bmpTemp.height

//计算压缩的比率

val scaleHeight = bubblePicHeight / height

//获取想要缩放的matrix

val matrix = Matrix()

matrix.postScale(scaleHeight, scaleHeight)

//获取新的bitmap

return Bitmap.createBitmap(bmpTemp, 0, 0, width, height, matrix, false)

}

View设置Drawable背景,再去掉背景之后,原来的padding并不会去掉,导致控件无法还原,所以要动态设置padding

更新列表数据闪动问题, 因为设置drawable背景比较耗时,且有可能设置不同padding的drawable,所以会导致列表view闪动, 解决方案是局部更新item,或者更新到具体的view

具体方案二和方案三哪种方案更合适,预研阶段进行了测试,看不出来有什么性能的差异,后续实际开发会持续进行测试验证。

android .9png聊天气泡,Android 关于点9图在气泡评论里使用的调研相关推荐

  1. android c聊天功能,Android实现简单C/S聊天室应用

    Android的网络应用:简单的C/S聊天室,供大家参考,具体内容如下 服务器端:提供两个类 创建ServerSocket监听的主类:MyServer.java 负责处理每个Socket通信的线程类: ...

  2. android点对点聊天软件,android + springboot实现点对点实时聊天

    前言 这周一就须要交android大实验做业,android大实验作了一个二手商城,可是功能太少了,android老师要求大实验要有一些特点.就想着实现一下实时聊天.而后就利用周日时间试了一下.and ...

  3. android蓝牙聊天设备,Android蓝牙开发——实现蓝牙聊天

    最近课上刚好需要做一个课程设计关于蓝牙的就挑选了个蓝牙聊天室,其实关键还是在于对蓝牙API的了解 一.蓝牙API 与蓝牙开发主要的相关类是以下四个 BluetoothAdapter 字面上则理解为蓝牙 ...

  4. android局域网聊天毕业设计,Android基于wifi模块的局域网聊天以及文件传输app

    [实例简介] 一款基于wifi模块的局域网实时聊天以及文件互传的安卓app,能实现热点创建,热点连接,文件传输,实时通讯等功能. [实例截图] [核心代码] MyFeiGe2.0 └── MyFeiG ...

  5. android气泡聊天框大小,android – 备用聊天气泡宽度

    我正在开发一个聊天类型的应用程序我正在使用两个不同的九个补丁来聊天泡泡主要消息和响应.我面临的问题是根据消息长度自动包装气泡的宽度.以下是我的主要布局: android:orientation=&qu ...

  6. 微信视频气泡 android,变变微信聊天气泡

    变变微信聊天气泡是一款非常好用的气泡美化软件,让你的聊天框看起来更加个性化,不需要ROOT即可完成气泡设计,更具趣味性,喜欢的小伙们快来下载试试吧! 软件详情 让气泡在你的指尖跳跃吧! 变变聊天气泡( ...

  7. android高德地图气泡,[置顶] Android-高德地图-显示气泡框

    现在的聊天框大多都是气泡框,气泡框长相可爱,有良好的用户体验. 如何把气泡框应用于地图上呢? 步骤一:首先要定义我们的气泡框布局,也就是所谓的layout. popup.xml: android:ba ...

  8. android仿微信聊天功能,Android高仿微信聊天界面代码分享

    微信聊天现在非常火,是因其界面漂亮吗,哈哈,也许吧.微信每条消息都带有一个气泡,非常迷人,看起来感觉实现起来非常难,其实并不难.下面小编给大家分享实现代码. 先给大家展示下实现效果图: OK,下面我们 ...

  9. android 微信高仿,Android高仿微信聊天界面代码分享

    微信聊天现在非常火,是因其界面漂亮吗,哈哈,也许吧.微信每条消息都带有一个气泡,非常迷人,看起来感觉实现起来非常难,其实并不难.下面小编给大家分享实现代码. 先给大家展示下实现效果图: OK,下面我们 ...

最新文章

  1. 自绘制HT For Web ComboBox下拉框组件
  2. Epox 8RDA3G主板奇怪的问题
  3. Docker Review - Docker 部署 Spring Boot 项目
  4. mysql数据导入导出方法总结
  5. [转载]-如何向妻子解释OOD
  6. 国内淘宝镜像 cnpm转npm
  7. mysql在线搭建从库_Mysql主从库搭建
  8. 数据库设计:数据库设计的基本步骤介绍
  9. 敲黑板!vue3重点!一文了解Composition API新特性:ref、toRef、toRefs
  10. jMatter:提高开发速度10倍!
  11. 纯前端集成视频会议和聊天室
  12. 宠物店 java 报告_宠物店社会实践报告通用范文
  13. html斜线背景,PS斜线底纹如何制作?
  14. linux使用sts4,视听说sts4-4
  15. recvfrom的addrlen参数
  16. 一、多媒体技术的基础本章小结
  17. STM32CubeMX系列08——SPI通信(W25Q64、NRF24L01无线模块)
  18. Redis在linux上安装教程,超级详细
  19. vmware 虚拟化Intel VT-x/EPT选项
  20. 打开System x服务器的IMM2 Web界面

热门文章

  1. Numpy入门(九):np.corrcoef()用法
  2. 什么是CMA检测报告
  3. matlab里表示颜色,求助:怎么画用颜色表示数值的图
  4. 关于EZDML的数据类型
  5. 给中国学生的第五封信
  6. vc向richedit控件写RTF格式内容(表格)
  7. 操作系统重要概念——并发性
  8. 实验9 人口预测与数据拟合(最小二乘法)
  9. Quest3d的项目管理
  10. javascript做游戏_我用JavaScript构建了一个角色扮演游戏。 你也可以 这是如何做。...