次元画社(二),瀑布流照片墙的实现

张天宇 on 2018-01-17

一、基础准备

1.素材准备

1.空Activity的新建,参照上篇

2.准备照片

​ 可以网络获取,也可从自己服务器取,这里我放置在了自己的树莓派上,因为性能带宽问题加载较慢,请务必换成自己寻找的网络照片。

​ 新建一个Image类放地址,后期将改作Json获取解析,以便后期数据更新。

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
public class Images {
public final static String[] imageUrls = new String[]{
"http://zhangty1996.imwork.net:26388/image/1.jpg",
"http://zhangty1996.imwork.net:26388/image/2.jpg",
"http://zhangty1996.imwork.net:26388/image/3.jpg",
"http://zhangty1996.imwork.net:26388/image/4.jpg",
"http://zhangty1996.imwork.net:26388/image/5.jpg",
"http://zhangty1996.imwork.net:26388/image/6.jpg",
"http://zhangty1996.imwork.net:26388/image/7.jpg",
"http://zhangty1996.imwork.net:26388/image/8.jpg",
"http://zhangty1996.imwork.net:26388/image/9.jpg",
"http://zhangty1996.imwork.net:26388/image/10.jpg",
"http://zhangty1996.imwork.net:26388/image/11.jpg",
"http://zhangty1996.imwork.net:26388/image/12.jpg",
"http://zhangty1996.imwork.net:26388/image/13.jpg",
"http://zhangty1996.imwork.net:26388/image/14.jpg",
"http://zhangty1996.imwork.net:26388/image/15.jpg",
"http://zhangty1996.imwork.net:26388/image/16.jpg",
"http://zhangty1996.imwork.net:26388/image/17.jpg",
"http://zhangty1996.imwork.net:26388/image/18.jpg",
"http://zhangty1996.imwork.net:26388/image/19.jpg",
"http://zhangty1996.imwork.net:26388/image/20.jpg",
"http://zhangty1996.imwork.net:26388/image/21.jpg",
"http://zhangty1996.imwork.net:26388/image/22.jpg",
"http://zhangty1996.imwork.net:26388/image/23.jpg",
"http://zhangty1996.imwork.net:26388/image/24.jpg",
"http://zhangty1996.imwork.net:26388/image/25.jpg",
"http://zhangty1996.imwork.net:26388/image/26.jpg",
"http://zhangty1996.imwork.net:26388/image/27.jpg",
"http://zhangty1996.imwork.net:26388/image/28.jpg",
"http://zhangty1996.imwork.net:26388/image/29.jpg",
"http://zhangty1996.imwork.net:26388/image/30.jpg",
"http://zhangty1996.imwork.net:26388/image/31.jpg",
"http://zhangty1996.imwork.net:26388/image/32.jpg",
"http://zhangty1996.imwork.net:26388/image/33.jpg",
"http://zhangty1996.imwork.net:26388/image/34.jpg",
"http://zhangty1996.imwork.net:26388/image/35.jpg",
"http://zhangty1996.imwork.net:26388/image/36.jpg",
"http://zhangty1996.imwork.net:26388/image/37.jpg",
"http://zhangty1996.imwork.net:26388/image/38.jpg",
"http://zhangty1996.imwork.net:26388/image/39.jpg",
"http://zhangty1996.imwork.net:26388/image/40.jpg"
};
}

二、代码编写

1.ImageLoader类

​ 对图片加载进行管理,参考自郭神博客。

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
public class ImageLoader {
// 图片缓存技术的核心类,用于缓存所有下载好的图片,在程序内存达到设定值时会将最少最近使用的图片移除掉
private static LruCache<String, Bitmap> mMemoryCache;
// ImageLoader的实例
private static ImageLoader mImageLoader;

private ImageLoader() {
// 获取应用程序最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
// 设置图片缓存大小为程序最大可用内存的1/8
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount();
}
};
}

// 获取ImageLoader的实例
public static ImageLoader getInstance() {
if (mImageLoader == null) {
mImageLoader = new ImageLoader();
}
return mImageLoader;
}

// 将一张图片存储到LruCache中
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}

// 从LruCache中获取一张图片,如果不存在就返回null。
public Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}

public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth) {
// 源图片的宽度
final int width = options.outWidth;
int inSampleSize = 1;
if (width > reqWidth) {
// 计算出实际宽度和目标宽度的比率
final int widthRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = widthRatio;
}
return inSampleSize;
}

public static Bitmap decodeSampledBitmapFromResource(String pathName,int reqWidth) {
// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
// 调用上面定义的方法计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth);
//使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(pathName, options);
}

// 根据URL获取bitmap
public Bitmap returnBitMap(String url) {
URL myFileUrl = null;
Bitmap bitmap = null;
InputStream is = null;
try {
myFileUrl = new URL(url);
} catch (MalformedURLException e) {
e.printStackTrace();
}
try {
HttpURLConnection conn = (HttpURLConnection) myFileUrl.openConnection();
conn.setDoInput(true);
conn.connect();
is = conn.getInputStream();
bitmap = BitmapFactory.decodeStream(is);
is.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null)
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return bitmap;
}
}
// 这里我们将ImageLoader类设成单例,并在构造函数中初始化了LruCache类,把它的最大缓存容量设为最大可用内存的1/8。然后又提供了其它几个方法可以操作LruCache,以及对图片进行压缩和读取。

2.MyScrollView类

​ 滚动视图,较为简单的滑动空间,用于上下滑加载新的图片。

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
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
public class MyScrollView extends ScrollView implements OnTouchListener {
// 每页要加载的图片数量
public static final int PAGE_SIZE = 20;
// 记录当前已加载到第几页
private int page;
// 每一列的宽度
private int columnWidth;
// 当前第一列的高度
private int firstColumnHeight;
// 当前第二列的高度
private int secondColumnHeight;
// 当前第三列的高度
private int thirdColumnHeight;
// 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
private boolean loadOnce;
// 对图片进行管理的工具类
private ImageLoader imageLoader;
// 第一列的布局
private LinearLayout firstColumn;
// 第二列的布局
private LinearLayout secondColumn;
// 第三列的布局
private LinearLayout thirdColumn;
// 记录所有正在下载或等待下载的任务
private static Set<LoadImageTask> taskCollection;
// MyScrollView下的直接子布局
private static View scrollLayout;
// MyScrollView布局的高度
private static int scrollViewHeight;
// 记录上垂直方向的滚动距离
private static int lastScrollY = -1;
// 记录所有界面上的图片,用以可以随时控制对图片的释放
private List<ImageView> imageViewList = new ArrayList<ImageView>();

// 在Handler中进行图片可见性检查的判断,以及加载更多图片的操作
private static Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
MyScrollView myScrollView = (MyScrollView) msg.obj;
int scrollY = myScrollView.getScrollY();
// 如果当前的滚动位置和上次相同,表示已停止滚动
if (scrollY == lastScrollY) {
// 当滚动的最底部,并且当前没有正在下载的任务时,开始加载下一页的图片
if (scrollViewHeight + scrollY >= scrollLayout.getHeight()
&& taskCollection.isEmpty()) {
myScrollView.loadMoreImages();
}
myScrollView.checkVisibility();
} else {
lastScrollY = scrollY;
Message message = new Message();
message.obj = myScrollView;
// 5毫秒后再次对滚动位置进行判断
handler.sendMessageDelayed(message, 5);
}
}

};

//MyScrollView的构造函数
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
imageLoader = ImageLoader.getInstance();
taskCollection = new HashSet<LoadImageTask>();
setOnTouchListener(this);
}

// 进行一些关键性的初始化操作,获取MyScrollView的高度,以及得到第一列的宽度值。并在这里开始加载第一页的图片
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && !loadOnce) {
scrollViewHeight = getHeight();
scrollLayout = getChildAt(0);
firstColumn = findViewById(R.id.first_column);
secondColumn = findViewById(R.id.second_column);
thirdColumn = findViewById(R.id.third_column);
columnWidth = firstColumn.getWidth();
loadOnce = true;
loadMoreImages();
}
}

// 监听用户的触屏事件,如果用户手指离开屏幕则开始进行滚动检测
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
Message message = new Message();
message.obj = this;
handler.sendMessageDelayed(message, 5);
}
return false;
}

// 开始加载下一页的图片,每张图片都会开启一个异步线程去下载
public void loadMoreImages() {
if (hasSDCard()) {
int startIndex = page * PAGE_SIZE;
int endIndex = page * PAGE_SIZE + PAGE_SIZE;
if (startIndex < Images.imageUrls.length) {
Toast.makeText(getContext(), "(ง •_•)ง努力加载...", Toast.LENGTH_SHORT).show();
if (endIndex > Images.imageUrls.length) {
endIndex = Images.imageUrls.length;
}
for (int i = startIndex; i < endIndex; i++) {
LoadImageTask task = new LoadImageTask();
taskCollection.add(task);
task.execute(i);
}
page++;
} else {
Toast.makeText(getContext(), ">︿<没有更多了", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(getContext(), "未发现SD卡", Toast.LENGTH_SHORT).show();
}
}

// 遍历imageViewList中的每张图片,对图片的可见性进行检查,如果图片已经离开屏幕可见范围,则将图片替换成一张空图
public void checkVisibility() {
for (int i = 0; i < imageViewList.size(); i++) {
ImageView imageView = imageViewList.get(i);
int borderTop = (Integer) imageView.getTag(R.string.border_top);
int borderBottom = (Integer) imageView.getTag(R.string.border_bottom);
if (borderBottom > getScrollY() && borderTop < getScrollY() + scrollViewHeight) {
String imageUrl = (String) imageView.getTag(R.string.image_url);
Bitmap bitmap = imageLoader.getBitmapFromMemoryCache(imageUrl);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
LoadImageTask task = new LoadImageTask(imageView);
task.execute(i);
}
} else {
imageView.setImageResource(R.mipmap.empty_photo);
}
}
}

// 判断手机是否有SD卡
private boolean hasSDCard() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}

// 异步下载图片的任务。
class LoadImageTask extends AsyncTask<Integer, Void, Bitmap> {
// 图片的URL地址
private String mImageUrl;
// 可重复使用的ImageView
private ImageView mImageView;
// 记录每个图片对应的位置
private int mItemPosition;
public LoadImageTask() {
}
// 将可重复使用的ImageView传入
public LoadImageTask(ImageView imageView) {
mImageView = imageView;
}

@Override
protected Bitmap doInBackground(Integer... params) {
mItemPosition = params[0];
mImageUrl = Images.imageUrls[mItemPosition];
Bitmap imageBitmap = imageLoader.getBitmapFromMemoryCache(mImageUrl);
if (imageBitmap == null) {
imageBitmap = loadImage(mImageUrl);

}
return imageBitmap;
}

@Override
protected void onPostExecute(Bitmap bitmap) {
if (bitmap != null) {
double ratio = bitmap.getWidth() / (columnWidth * 1.0);
int scaledHeight = (int) (bitmap.getHeight() / ratio);
addImage(bitmap, columnWidth, scaledHeight);
}
taskCollection.remove(this);
}

// 根据传入的URL,对图片进行加载。如果这张图片已经存在于SD卡中,则直接从SD卡里读取,否则就从网络上下载
private Bitmap loadImage(String imageUrl) {
File imageFile = new File(getImagePath(imageUrl));
if (!imageFile.exists()) {
downloadImage(imageUrl);
}
if (imageUrl != null) {
Bitmap bitmap = ImageLoader.decodeSampledBitmapFromResource(imageFile.getPath(),
columnWidth);
if (bitmap != null) {
imageLoader.addBitmapToMemoryCache(imageUrl, bitmap);
return bitmap;
}
}
return null;
}

// 向ImageView中添加一张图片
private void addImage(Bitmap bitmap, int imageWidth, int imageHeight) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(imageWidth,
imageHeight);
if (mImageView != null) {
mImageView.setImageBitmap(bitmap);
} else {
ImageView imageView = new ImageView(getContext());
imageView.setLayoutParams(params);
imageView.setImageBitmap(bitmap);
imageView.setScaleType(ScaleType.FIT_XY);
imageView.setPadding(5, 5, 5, 5);
imageView.setTag(R.string.image_url, mImageUrl);
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getContext(), ImageDetailsActivity.class);
intent.putExtra("image_position", mItemPosition);
getContext().startActivity(intent);
}
});
findColumnToAdd(imageView, imageHeight).addView(imageView);
imageViewList.add(imageView);
}
}

// 找到此时应该添加图片的一列。原则就是对三列的高度进行判断,当前高度最小的一列就是应该添加的一列
private LinearLayout findColumnToAdd(ImageView imageView, int imageHeight) {
if (firstColumnHeight <= secondColumnHeight) {
if (firstColumnHeight <= thirdColumnHeight) {
imageView.setTag(R.string.border_top, firstColumnHeight);
firstColumnHeight += imageHeight;
imageView.setTag(R.string.border_bottom, firstColumnHeight);
return firstColumn;
}
imageView.setTag(R.string.border_top, thirdColumnHeight);
thirdColumnHeight += imageHeight;
imageView.setTag(R.string.border_bottom, thirdColumnHeight);
return thirdColumn;
} else {
if (secondColumnHeight <= thirdColumnHeight) {
imageView.setTag(R.string.border_top, secondColumnHeight);
secondColumnHeight += imageHeight;
imageView.setTag(R.string.border_bottom, secondColumnHeight);
return secondColumn;
}
imageView.setTag(R.string.border_top, thirdColumnHeight);
thirdColumnHeight += imageHeight;
imageView.setTag(R.string.border_bottom, thirdColumnHeight);
return thirdColumn;
}
}

// 将图片下载到SD卡缓存起来
private void downloadImage(String imageUrl) {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Log.d("TAG", "monted sdcard");
} else {
Log.d("TAG", "has no sdcard");
}
HttpURLConnection con = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
BufferedInputStream bis = null;
File imageFile = null;
try {
URL url = new URL(imageUrl);
con = (HttpURLConnection) url.openConnection();
con.setConnectTimeout(5 * 1000);
con.setReadTimeout(15 * 1000);
con.setDoInput(true);
con.setDoOutput(true);
bis = new BufferedInputStream(con.getInputStream());
imageFile = new File(getImagePath(imageUrl));
fos = new FileOutputStream(imageFile);
bos = new BufferedOutputStream(fos);
byte[] b = new byte[1024];
int length;
while ((length = bis.read(b)) != -1) {
bos.write(b, 0, length);
bos.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
if (con != null) {
con.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (imageFile != null) {
Bitmap bitmap = ImageLoader.decodeSampledBitmapFromResource(imageFile.getPath(),
columnWidth);
if (bitmap != null) {
imageLoader.addBitmapToMemoryCache(imageUrl, bitmap);
}
}
}

// 获取图片的本地存储路径
private String getImagePath(String imageUrl) {
int lastSlashIndex = imageUrl.lastIndexOf("/");
String imageName = imageUrl.substring(lastSlashIndex + 1);
String imageDir = Environment.getExternalStorageDirectory().getPath()
+ "/PhotoWallFalls/";
File file = new File(imageDir);
if (!file.exists()) {
file.mkdirs();
}
String imagePath = imageDir + imageName;
return imagePath;
}
}
}

​ MyScrollView是实现瀑布流照片墙的核心类。首先它是继承自ScrollView的,这样就允许用户可以通过滚动的方式来浏览更多的图片。这里提供了一个loadMoreImages()方法,是专门用于加载下一页的图片的,因此在onLayout()方法中我们要先调用一次这个方法,以初始化第一页的图片。然后在onTouch方法中每当监听到手指离开屏幕的事件,就会通过一个handler来对当前ScrollView的滚动状态进行判断,如果发现已经滚动到了最底部,就会再次调用loadMoreImages()方法去加载下一页的图片。

​ 那我们就要来看一看loadMoreImages()方法的内部细节了。在这个方法中,使用了一个循环来加载这一页中的每一张图片,每次都会开启一个LoadImageTask,用于对图片进行异步加载。然后在LoadImageTask中,首先会先检查一下这张图片是不是已经存在于SD卡中了,如果还没存在,就从网络上下载,然后把这张图片存放在LruCache中。接着将这张图按照一定的比例进行压缩,并找出当前高度最小的一列,把压缩后的图片添加进去就可以了。

​ 另外,为了保证照片墙上的图片都能够合适地被回收,这里还加入了一个可见性检查的方法,即checkVisibility()方法。这个方法的核心思想就是检查目前照片墙上的所有图片,判断出哪些是可见的,哪些是不可见。然后将那些不可见的图片都替换成一张空图,这样就可以保证程序始终不会占用过高的内存。当这些图片又重新变为可见的时候,只需要再从LruCache中将这些图片重新取出即可。如果某张图片已经从LruCache中被移除了,就会开启一个LoadImageTask,将这张图片重新加载到内存中。

3.流布局

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
<!--activity_main.xml-->
<RelativeLayout
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"
tools:context="com.android.zty.ssssss.MainActivity">
<com.android.zty.ssssss.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/first_column"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
</LinearLayout>
<LinearLayout
android:id="@+id/second_column"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
</LinearLayout>
<LinearLayout
android:id="@+id/third_column"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>

</com.android.zty.ssssss.MyScrollView>
<!--点击展开侧滑菜单-->
<ImageButton
android:id="@+id/btn_left_menu"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
android:background="@mipmap/logo_cir"/>
</RelativeLayout>

4.使用权限

1
2
3
<!--AndroidManifest.xml-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />

三、效果图

效果图