需求场景
在平时我们浏览网站时或者在看Tiktok或者抖音上会发现一些图片视频为了适应横竖屏加了两边的高斯模糊。
另外一个场景是应用首页在展示包含横竖页面时,数据是由第三方网站返回的图片样式,服务端不能自己返回统一横竖向图时,我们的列表就需要去适配这种情况。
1.单一图片情况
2.模仿首页列表效果
需求的要点在于如果是Item是横向的 则接到横图时不做处理,竖图时再进行处理 反之一样。
实现思路
因为在存在高刷的应用环境 所以有以下要求 1.高斯模糊的内存占用和速度必须要快,不能过于消耗时间和内存CPU资源 2.图片必须能被缓存,所以只能考虑新做一张图利用图片框架进行缓存和显示 3.也可以做图片在过程中进行其他操作,比如圆角等等
实现工具
1.Glide 图片加载框架 提供网络图片加载,图片缓存以及图片转化功能 2.RenderScript 使用android原生类实现,版本要求4+,对现在大部分手机没有压力,其他方案要不就兼容性不好,要不就效率太低 包括Glide内部使用的优势fastblur 效率达不到 参考这篇文章 Android 图片高斯模糊解决方案
实现步骤
MainActivity
主页面 没有什么多说的 主要显示组件和跳转页面
public class MainActivity extends AppCompatActivity {
//横图网址
public static String PIC_ADDRESS_HORIZONTAL = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.yhsport.com%2Fdata%2Fupload%2Fimage%2F20200528%2F1590634830974906.jpg&refer=http%3A%2F%2Fwww.yhsport.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646044342&t=7743dbf24a863c141b45526b856ffc75";
//竖图网址
public static String PIC_ADDRESS_VERTICAL = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic3.zhimg.com%2F50%2Fv2-5ce9c04caf8a6875299912d0b30e3b6a_hd.jpg&refer=http%3A%2F%2Fpic3.zhimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1649408053&t=af0563051c4767f3c54f1f7d901cd019";
//原图
ImageView original;
//横向模糊
ImageView hor_blur;
//竖向模糊
ImageView ver_blur;
//设置横图
Button set_hor;
//设置竖图
Button set_ver;
//前往列表页面
Button go_list;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
original = findViewById(R.id.original);
hor_blur = findViewById(R.id.hor_blur);
ver_blur = findViewById(R.id.ver_blur);
set_hor = findViewById(R.id.button);
set_ver = findViewById(R.id.button2);
go_list = findViewById(R.id.button1);
set_hor.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ImageLoader.loadImage(original, PIC_ADDRESS_HORIZONTAL);
ImageLoader.loadBlurImageHorizontal(hor_blur, PIC_ADDRESS_HORIZONTAL,15,hor_blur.getWidth(),hor_blur.getHeight());
ImageLoader.loadBlurImageVertical(ver_blur, PIC_ADDRESS_HORIZONTAL,15,ver_blur.getWidth(),ver_blur.getHeight());
}
});
set_ver.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ImageLoader.loadImage(original, PIC_ADDRESS_VERTICAL);
ImageLoader.loadBlurImageHorizontal(hor_blur, PIC_ADDRESS_VERTICAL,15,hor_blur.getWidth(),hor_blur.getHeight());
ImageLoader.loadBlurImageVertical(ver_blur, PIC_ADDRESS_VERTICAL,15,ver_blur.getWidth(),ver_blur.getHeight());
}
});
go_list.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,ListActivity.class);
startActivity(intent);
}
});
}
@Override
protected void onResume() {
super.onResume();
}
}
ListActivity
用于模拟显示横向列表和网格列表时 混合加入横竖图时的显示效果
public class ListActivity extends AppCompatActivity {
RecyclerView mRecyclerViewHor, mRecyclerViewVer;
RecyclerView.Adapter mAdapterHor, mAdapterVer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list);
mAdapterHor = new HorAdapter();
mRecyclerViewHor = (RecyclerView) findViewById(R.id.rv1);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
mRecyclerViewHor.setLayoutManager(linearLayoutManager);
mRecyclerViewHor.setAdapter(mAdapterHor);
mAdapterVer = new VerAdapter();
mRecyclerViewVer = (RecyclerView) findViewById(R.id.rv2);
mRecyclerViewVer.setLayoutManager(new GridLayoutManager(this,6));
mRecyclerViewVer.setAdapter(mAdapterVer);
}
class HorAdapter extends RecyclerView.Adapter<HorAdapter.MyViewHolder>
{
int i = 0;
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
MyViewHolder holder = new MyViewHolder(LayoutInflater.from(
ListActivity.this).inflate(R.layout.list_item, parent,
false));
return holder;
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position)
{
if(i++%2==0){
ImageLoader.loadBlurImageHorizontal(holder.iv, MainActivity.PIC_ADDRESS_VERTICAL,15,holder.iv.getWidth(),holder.iv.getHeight());
}else{
ImageLoader.loadBlurImageHorizontal(holder.iv, MainActivity.PIC_ADDRESS_HORIZONTAL,15,holder.iv.getWidth(),holder.iv.getHeight());
}
}
@Override
public int getItemCount()
{
return 24;
}
class MyViewHolder extends RecyclerView.ViewHolder
{
ImageView iv;
public MyViewHolder(View view)
{
super(view);
iv = (ImageView) view.findViewById(R.id.id_image);
}
}
}
class VerAdapter extends RecyclerView.Adapter<VerAdapter.MyViewHolder>
{
int i = 0;
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
MyViewHolder holder = new MyViewHolder(LayoutInflater.from(
ListActivity.this).inflate(R.layout.list_item2, parent,
false));
return holder;
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position)
{
if(i++%2==0){
ImageLoader.loadBlurImageVertical(holder.iv, MainActivity.PIC_ADDRESS_VERTICAL,15,holder.iv.getWidth(),holder.iv.getHeight());
}else{
ImageLoader.loadBlurImageVertical(holder.iv, MainActivity.PIC_ADDRESS_HORIZONTAL,15,holder.iv.getWidth(),holder.iv.getHeight());
}
}
@Override
public int getItemCount()
{
return 48;
}
class MyViewHolder extends RecyclerView.ViewHolder
{
ImageView iv;
public MyViewHolder(View view)
{
super(view);
iv = (ImageView) view.findViewById(R.id.id_image);
}
}
}
@Override
protected void onResume() {
super.onResume();
}
}
BlurUtil
这个类负责图片模糊的处理
首先是公开调用方法 loadBlurImageVertical 注意这里需要传一下控件的宽高,用于接下来的计算过程
/**
* 高斯模糊图片
* 如果是方向和显示组件一样 则正常显示
* 如果不是 则图片缩放居中 然后高斯模糊center_crop以后的图片做背景
*
* @param img 图片空间
* @param url 图片地址
* @param radiusDp 圆角角度
* @param viewWidth 控件宽度
* @param viewHeight 控件高度
*/
public static void loadBlurImageVertical(ImageView img, String url,
float radiusDp, int viewWidth, int viewHeight) {
RoundedCorners roundedCorners = new RoundedCorners((int) radiusDp);
RequestOptions requestOptions = new RequestOptions();
Glide.with(img).load(url).placeholder(R.drawable.ic_launcher_background).apply(requestOptions.
centerCrop().transform(roundedCorners,new BlurImageVerticalTransformation(img,viewWidth,viewHeight))).into(img);
}
这里会用到Glide的transform方法 这里需要传一个实现BitmapTransformation的类 这个类用于拿到图片以后的进行我们想要的一些自定义处理
/**
* Glide竖图模糊图片转换类
*/
static class BlurImageVerticalTransformation extends BitmapTransformation {
private static final String ID = "com.ljq.blurimage";
private final byte[] ID_BYTES = ID.getBytes(CHARSET);
private ImageView img;
private int viewWidth;
private int viewHeight;
public BlurImageVerticalTransformation(ImageView img,
int viewWidth,
int viewHeight) {
this.img = img;
this.viewWidth = viewWidth;
this.viewHeight = viewHeight;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(ID_BYTES);
}
@Override
public boolean equals(Object o) {
return o instanceof BlurImageVerticalTransformation;
}
@Override
public int hashCode() {
return ID.hashCode();
}
@Override
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
Bitmap bitmap;
int imgw = img.getWidth() == 0 ? viewWidth : img.getWidth();
int imgh = img.getHeight() == 0 ? viewHeight : img.getHeight();
if (toTransform.getWidth() < toTransform.getHeight()) {////如果是竖图不操作
Log.d(TAG, "BlurImageVerticalTransformation 111 bitmapBottom.getConfig() " +
toTransform.getConfig());
// BitmapUtil.zoomImgCentCrop(toTransform, imgw, imgh);
bitmap = toTransform;
}
else {//如果是横图就进行操作
//1.原图进行裁剪做成背景图
Bitmap bitmapBottom = BitmapUtil
.zoomImgCentCrop(toTransform, imgw,
imgh);
Log.d(TAG, "BlurImageVerticalTransformation 222 bitmapBottom.getConfig() " +
bitmapBottom.getConfig());
//2.把背景图模糊
bitmapBottom = BitmapUtil
.blurBitmap(img.getContext(), bitmapBottom,
BLUR_RADIUS);
Bitmap bitmapTop;
//3.算出图片比例
float ratio =
Float.valueOf(bitmapBottom.getWidth()) /
Float.valueOf(toTransform.getWidth());
//4.缩放图片作为前景图
bitmapTop = BitmapUtil.scaleBitmap(toTransform, ratio);
//5.重叠图片
bitmap = BitmapUtil
.overlayBitmapVertical(bitmapBottom, bitmapTop);
}
return bitmap;
}
}
主要看transform方法 首先取得宽高 如果是朝向一样就不用操作 如果不一样 根据对图片结构的分析 则进行下面五步
- 原图进行裁剪做成背景图
- 把背景图模糊
- 根据控件大小算出缩放比例
- 缩放图片作为前景图 把图片进行缩放剪裁 这样图片就可以合适的放在图片中央不会遗漏内容
- 重叠图片
这里只有重叠图片的时候操作有一些区别,其他步骤都是一样的
loadBlurImageHorizontal是镜像方法,逻辑基本一样,这里讲一个就行、
BitmapUtil
模糊的核心方法 这个代码基本都在网上有 但是需要注意的一点是
在实际的应用中 发现如果图片本身是RGB_565 在有些手机系统上会模糊失败 这里需要统一转成RGB_888 这个问题当时找了很久 算是一个坑 当前看源码看了好久
public static Bitmap blurBitmap(Context context, Bitmap bitmap, float blurRadius) {
//如果图片本身是RGB_565 在有些手机系统上会模糊失败 这里需要统一转成RGB_888
if (bitmap.getConfig() == Bitmap.Config.RGB_565) {
// long start = System.currentTimeMillis();
bitmap = rgb565to888(bitmap);
// long end = System.currentTimeMillis();
// Log.d(TAG," spend time 888 : " +(end-start));
}
// 1.创建RenderScript内核对象
RenderScript rs = RenderScript.create(context);
// 2. 由于RenderScript并没有使用VM来分配内存,所以需要使用Allocation类来创建和分配内存空间,创建Allocation对象的时候其实内存是空的,需要使用copyTo()将数据填充进去
Allocation input = Allocation.createFromBitmap(rs, bitmap);
//追查问题代码
// Element e = input.getElement();
// return ((mSize == e.mSize) &&
// (mType != Element.DataType.NONE) &&
// (mType == e.mType) &&
// (mVectorSize == e.mVectorSize));
// Element u84 = Element.U8_4(rs);
// Element u8 = Element.U8(rs);
// e.getBytesSize();
// e.getDataType();
// e.getVectorSize();
// Log.d(TAG,
// " e.getBytesSize() " + e.getBytesSize() + " e.getDataType() " + e.getDataType() + " e.getVectorSize() "+ e.getVectorSize() + " bitmap.getConfig() " + bitmap.getConfig());
//
// Log.d(TAG,
// " u84.getBytesSize() " + u84.getBytesSize() + " e.getDataType() " + u84.getDataType() + " e.getVectorSize() "+ u84.getVectorSize());
//
// Log.d(TAG,
// " u8.getBytesSize() " + u8.getBytesSize() + " u8.getDataType() " + u8.getDataType() + " e.getVectorSize() "+ u8.getVectorSize());
// 3. 创建相同类型的Allocation对象用来输出
Type type = input.getType();
Allocation output = Allocation.createTyped(rs, type);
// 4. 创建一个模糊效果的RenderScript的工具对象,第二个参数Element相当于一种像素处理的算法,高斯模糊的话用这个就好
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
// 5. 设置渲染的模糊程度, 25f是最大模糊度
blurScript.setRadius(blurRadius);
// output.getElement().isCompatible()
// 6. 设置blurScript对象的输入内存
blurScript.setInput(input);
// 7. 将输出数据保存到输出内存中
blurScript.forEach(output);
// 8. 将数据填充到bitmap中
output.copyTo(bitmap);
// 9. 销毁它们释放内存
input.destroy();
output.destroy();
blurScript.destroy();
rs.destroy();
//在Allocation会destroy掉 不用重复destroy 会报错
// type.destroy();
return bitmap;
}
其他的需要注意的地方就是可以在原图进行裁剪做成背景图之前可以使用simpleBitmap方法去有效的减少模糊使用的时间和资源,因为背景图不重要而且压缩以后做成的高斯模糊图区别不大
/**
* 按比例低质量图片减少内存占用
* @param bitmap
* @param inSampleSize
* @return
*/
private static Bitmap simpleBitmap(Bitmap bitmap,int inSampleSize) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG,100,baos);
byte[] bytes = baos.toByteArray();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
options.inPreferredConfig = Bitmap.Config.ARGB_4444;
Bitmap sample_bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length,
options);
// bitmap.recycle();
// bitmap = null;
return sample_bitmap;
}
这样就完成以上的效果了 源代码在这里 github.com/ufo00l/like…
欢迎和大家交流学习