安卓图片模糊占位加载

先看效果图:

Untitled1

这个效果在知乎Medium 的 PC 端都有应用。

而且手机端和电脑端都可以通过相同的方式实现这个效果,核心逻辑都是先加载一张放大的缩略图,将其进行模糊处理,同时加载大图并添加一个监听,当大图加载完成的时候,将小图替换成大图。

这里我们直接使用 Glide 图片加载框架。

先简单的写个布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.zhanglf.badgenumberdemo.MainActivity">

<ImageView
android:scaleType="fitCenter"
android:id="@+id/iv_load"
android:layout_width="400dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:layout_marginTop="40dp"/>
<ImageView
android:scaleType="fitCenter"
android:id="@+id/iv_load2"
android:layout_width="400dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:layout_marginTop="40dp"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">

<Button
android:id="@+id/bt_load"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="LOAD"/>

<Button
android:id="@+id/bt_clean"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="40dp"
android:text="CLEAN"/>
</LinearLayout>
</LinearLayout>

单独封装一个 BlurImageLoader 类来实行图片模糊加载。(其实可以很简洁的,但是我看要传入的参数太多,再则为了试试之前的看的 Builder 设计模式,这里简单封装了一下,其实真的是画蛇添足,傻逼了,也不想改了 T T)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class BlurImageLoader {

private Context mContext;
private String mBlurImageUrl;
private String mNormalImageUrl;
private ImageView mImageView;

public static class Builder {
private final Context mContext;
private String mBlurImageUrl;
private String mNormalImageUrl;
private ImageView mImageView;

public Builder(Context context) {
this.mContext = context;
}

public Builder Loader(String blurImageUrl, String normalImageUrl) {
this.mBlurImageUrl = blurImageUrl;
this.mNormalImageUrl = normalImageUrl;
return this;
}

public Builder into(ImageView targetImageView) {
this.mImageView = targetImageView;
return this;
}

public BlurImageLoader builder() {
return new BlurImageLoader(this);
}
}

public BlurImageLoader(Builder builder) {
mContext = builder.mContext;
mBlurImageUrl = builder.mBlurImageUrl;
mNormalImageUrl = builder.mNormalImageUrl;
mImageView = builder.mImageView;
}

/**
* @param context Any context, will not be retained.
* @param imageView The target to load the resource into.
* @param blurImageUrl The thumbnail view of url.
* @param normalImageUrl The normal view of url
*/

public void Loader() {
Glide.with(mContext).load(mBlurImageUrl).bitmapTransform(new BlurTransformation(mContext, 5)).into(new GlideDrawableImageViewTarget(mImageView) {
@Override
public void onResourceReady(final GlideDrawable resource, final GlideAnimation<? super GlideDrawable> animation) {
super.onResourceReady(resource, animation);
Glide.with(mContext).load(mNormalImageUrl).placeholder(resource).crossFade().into(mImageView);
}
});
}

}

核心代码就是 Loader() 方法了,其中:

.bitmapTransform(new BlurTransformation(mContext, 5)) 这个新建类就是模糊的关键了。

他是我从 glide-transformations 这个强大的 Glide 加载方式辅助工具中的一个类中摘取出来的。

这个类 BlurTransformation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* Copyright (C) 2015 Wasabeef
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.renderscript.RSRuntimeException;

import com.bumptech.glide.Glide;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapResource;

public class BlurTransformation implements Transformation<Bitmap> {

private static int MAX_RADIUS = 25;
private static int DEFAULT_DOWN_SAMPLING = 1;

private Context mContext;
private BitmapPool mBitmapPool;

private int mRadius;
private int mSampling;

public BlurTransformation(Context context) {
this(context, Glide.get(context).getBitmapPool(), MAX_RADIUS, DEFAULT_DOWN_SAMPLING);
}

public BlurTransformation(Context context, BitmapPool pool) {
this(context, pool, MAX_RADIUS, DEFAULT_DOWN_SAMPLING);
}

public BlurTransformation(Context context, BitmapPool pool, int radius) {
this(context, pool, radius, DEFAULT_DOWN_SAMPLING);
}

public BlurTransformation(Context context, int radius) {
this(context, Glide.get(context).getBitmapPool(), radius, DEFAULT_DOWN_SAMPLING);
}

public BlurTransformation(Context context, int radius, int sampling) {
this(context, Glide.get(context).getBitmapPool(), radius, sampling);
}

public BlurTransformation(Context context, BitmapPool pool, int radius, int sampling) {
mContext = context.getApplicationContext();
mBitmapPool = pool;
mRadius = radius;
mSampling = sampling;
}

@Override
public Resource<Bitmap> transform(Resource<Bitmap> resource, int outWidth, int outHeight) {
Bitmap source = resource.get();

int width = source.getWidth();
int height = source.getHeight();
int scaledWidth = width / mSampling;
int scaledHeight = height / mSampling;

Bitmap bitmap = mBitmapPool.get(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
if (bitmap == null) {
bitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
}

Canvas canvas = new Canvas(bitmap);
canvas.scale(1 / (float) mSampling, 1 / (float) mSampling);
Paint paint = new Paint();
paint.setFlags(Paint.FILTER_BITMAP_FLAG);
canvas.drawBitmap(source, 0, 0, paint);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
try {
bitmap = RSBlur.blur(mContext, bitmap, mRadius);
} catch (RSRuntimeException e) {
bitmap = FastBlur.blur(bitmap, mRadius, true);
}
} else {
bitmap = FastBlur.blur(bitmap, mRadius, true);
}

return BitmapResource.obtain(bitmap, mBitmapPool);
}

@Override public String getId() {
return "BlurTransformation(radius=" + mRadius + ", sampling=" + mSampling + ")";
}
}

他有多种构造方式,常用的就是上面用到的,第一个传入上下文,第二次传入一个 int 值,这个 int 值代表图片的模糊程度。

对模糊程度没有概念的可以看这个网站,自己调整感受一下。

BlurTransformation 类中对 VERSION_CODES 进行判断,大于 18 的采用 RSBlur 进行模糊处理,也就是安卓自带的 ScriptIntrinsicBlur 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66


import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RSRuntimeException;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;

/**
* Copyright (C) 2017 Wasabeef
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

public class RSBlur {

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public static Bitmap blur(Context context, Bitmap bitmap, int radius) throws RSRuntimeException {
RenderScript rs = null;
Allocation input = null;
Allocation output = null;
ScriptIntrinsicBlur blur = null;
try {
rs = RenderScript.create(context);
rs.setMessageHandler(new RenderScript.RSMessageHandler());
input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE,
Allocation.USAGE_SCRIPT);
output = Allocation.createTyped(rs, input.getType());
blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));

blur.setInput(input);
blur.setRadius(radius);
blur.forEach(output);
output.copyTo(bitmap);
} finally {
if (rs != null) {
rs.destroy();
}
if (input != null) {
input.destroy();
}
if (output != null) {
output.destroy();
}
if (blur != null) {
blur.destroy();
}
}

return bitmap;
}
}

小于 18 的采用被广为使用的 FastBlur 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import android.graphics.Bitmap;

/**
* Copyright (C) 2015 Wasabeef
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

public class FastBlur {

public static Bitmap blur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) {

// Stack Blur v1.0 from
// http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
//
// Java Author: Mario Klingemann <mario at quasimondo.com>
// http://incubator.quasimondo.com
// created Feburary 29, 2004
// Android port : Yahel Bouaziz <yahel at kayenko.com>
// http://www.kayenko.com
// ported april 5th, 2012

// This is a compromise between Gaussian Blur and Box blur
// It creates much better looking blurs than Box Blur, but is
// 7x faster than my Gaussian Blur implementation.
//
// I called it Stack Blur because this describes best how this
// filter works internally: it creates a kind of moving stack
// of colors whilst scanning through the image. Thereby it
// just has to add one new block of color to the right side
// of the stack and remove the leftmost color. The remaining
// colors on the topmost layer of the stack are either added on
// or reduced by one, depending on if they are on the right or
// on the left side of the stack.
//
// If you are using this algorithm in your code please add
// the following line:
//
// Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>

Bitmap bitmap;
if (canReuseInBitmap) {
bitmap = sentBitmap;
} else {
bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
}

if (radius < 1) {
return (null);
}

int w = bitmap.getWidth();
int h = bitmap.getHeight();

int[] pix = new int[w * h];
bitmap.getPixels(pix, 0, w, 0, 0, w, h);

int wm = w - 1;
int hm = h - 1;
int wh = w * h;
int div = radius + radius + 1;

int r[] = new int[wh];
int g[] = new int[wh];
int b[] = new int[wh];
int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
int vmin[] = new int[Math.max(w, h)];

int divsum = (div + 1) >> 1;
divsum *= divsum;
int dv[] = new int[256 * divsum];
for (i = 0; i < 256 * divsum; i++) {
dv[i] = (i / divsum);
}

yw = yi = 0;

int[][] stack = new int[div][3];
int stackpointer;
int stackstart;
int[] sir;
int rbs;
int r1 = radius + 1;
int routsum, goutsum, boutsum;
int rinsum, ginsum, binsum;

for (y = 0; y < h; y++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
for (i = -radius; i <= radius; i++) {
p = pix[yi + Math.min(wm, Math.max(i, 0))];
sir = stack[i + radius];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rbs = r1 - Math.abs(i);
rsum += sir[0] * rbs;
gsum += sir[1] * rbs;
bsum += sir[2] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
}
stackpointer = radius;

for (x = 0; x < w; x++) {

r[yi] = dv[rsum];
g[yi] = dv[gsum];
b[yi] = dv[bsum];

rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;

stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];

routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];

if (y == 0) {
vmin[x] = Math.min(x + radius + 1, wm);
}
p = pix[yw + vmin[x]];

sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);

rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];

rsum += rinsum;
gsum += ginsum;
bsum += binsum;

stackpointer = (stackpointer + 1) % div;
sir = stack[(stackpointer) % div];

routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];

rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];

yi++;
}
yw += w;
}
for (x = 0; x < w; x++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
yp = -radius * w;
for (i = -radius; i <= radius; i++) {
yi = Math.max(0, yp) + x;

sir = stack[i + radius];

sir[0] = r[yi];
sir[1] = g[yi];
sir[2] = b[yi];

rbs = r1 - Math.abs(i);

rsum += r[yi] * rbs;
gsum += g[yi] * rbs;
bsum += b[yi] * rbs;

if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}

if (i < hm) {
yp += w;
}
}
yi = x;
stackpointer = radius;
for (y = 0; y < h; y++) {
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];

rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;

stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];

routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];

if (x == 0) {
vmin[y] = Math.min(y + r1, hm) * w;
}
p = x + vmin[y];

sir[0] = r[p];
sir[1] = g[p];
sir[2] = b[p];

rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];

rsum += rinsum;
gsum += ginsum;
bsum += binsum;

stackpointer = (stackpointer + 1) % div;
sir = stack[stackpointer];

routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];

rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];

yi += w;
}
}

bitmap.setPixels(pix, 0, w, 0, 0, w, h);

return (bitmap);
}
}

获取到了被模糊处理过后的缩略图之后,回到之前的 Loader() 方法中,我们重写了 onResourceReady() ,也就是当缩略图加载完成时,我们将该 resoure (注意这里的 resource 是已经被模糊处理过后的)放到再次调用 Glide图片加载的 .placeholder(resource) 中,当做是第二次加载图片的占位符。这里添加了一个 Glide 自带的过渡动画效果 .crossFade() 渐显。那么当大图加载完成的时候就会成功的替换之前的模糊小图了。

为了试验效果,我们给两个按钮添加点击事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mIvLoad1 = (ImageView) findViewById(R.id.iv_load);
mIvLoad2 = (ImageView) findViewById(R.id.iv_load2);

findViewById(R.id.bt_load).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
BlurImageLoader ivLoader1 = new BlurImageLoader.Builder(MainActivity.this).Loader(blurImageUrl1, normalImageUrl1).into(mIvLoad1).builder();
ivLoader1.Loader();
BlurImageLoader ivLoader2 = new BlurImageLoader.Builder(MainActivity.this).Loader(blurImageUrl2, normalImageUrl2).into(mIvLoad2).builder();
ivLoader2.Loader();
}
});

findViewById(R.id.bt_clean).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

Glide.clear(mIvLoad1);
Glide.clear(mIvLoad2);
mIvLoad1.setImageBitmap(null);
mIvLoad2.setImageBitmap(null);
clearCacheDiskSelf();
clearCacheMemory();
}
});

因为 Glide 自带的缓存效果,所以我们必须清除磁盘和内存的缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public boolean clearCacheDiskSelf() {
try {
if (Looper.myLooper() == Looper.getMainLooper()) {
new Thread(new Runnable() {
@Override
public void run() {
Glide.get(getApplicationContext()).clearDiskCache();
}
}).start();
} else {
Glide.get(getApplicationContext()).clearDiskCache();
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

public boolean clearCacheMemory() {
try {
if (Looper.myLooper() == Looper.getMainLooper()) { //只能在主线程执行
Glide.get(getApplicationContext()).clearMemory();
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

哦,对了,我们要有图片来源才能实验啊,加载一张图片我们需要一张大图和一张小图,这里我的资源是随便打开了一个 Medium 网站上的网页使用浏览器的元素检查功能,然后直接复制了 url

屏幕快照 2017-06-01 上午10.03.32

我弄了两张图的资源,也就是四张图,为了加载快一点,将其放到了我自己的七牛云中,下面是资源地址:

1
2
3
4
private String blurImageUrl1 = "http://on9hzfn6s.bkt.clouddn.com/zhanglffffff.jpeg";
private String blurImageUrl2 = "http://on9hzfn6s.bkt.clouddn.com/1-_qd4EhIwH.jpeg";
private String normalImageUrl1 = "http://on9hzfn6s.bkt.clouddn.com/zhanglffffffbbb.jpeg";
private String normalImageUrl2 = "http://on9hzfn6s.bkt.clouddn.com/1-_qd4EhIwHbAjqzKPHu8-Ig.jpeg";

然后就大功告成了(之前的效果图故意延时了一秒,这个是没有延时的)。

Untitled