Android小游戏从零开始——拼图(2):自由选择图片
前言
最近遭老罪了,进了几次医院,才发现人生又有什么大事呢,除了肉体的痛苦,其他的不过是价值观带来的罢了。言归正传,之前笔者写的拼图小游戏,过于简单,现在就给他丰富一下。
主要改动如下:
(1)自定义权限工具类动态获取权限。
(2)自定义图片加载类:三级缓存。
(4)自定义方形图片类。
(3)使用Recyclerview展示图片。
效果
拼图游戏
权限获取
在安卓6以前,只需在配置AndroidManifest.xml中注册了权限,app就默认用户同意了该权限,可以直接使用。而在安卓7及以后,除了在配置文件注册以外,部分危险权限(比如相机使用权限、文件读写权限等)还需要进行动态申请。
动态权限的申请主要是调用了Activity的requestPermissions方法,然后重写Activity的onRequestPermissionsResult方法,在里面处理权限申请结果。
public class PermissionsUtils {
private static final int REQUEST_PERMISSION_CODE = 1;
public static void requestRequiredPermissions(Activity activity, String[] permissions) {
if (permissions.length != 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.requestPermissions(permissions, REQUEST_PERMISSION_CODE);
}
}
public static void handleRequestPermissionsResult(Activity activity, int code, String[] permissions, GrantedListener grantedListener) {
if (code == REQUEST_PERMISSION_CODE) {
boolean requestFinish = true;
if (permissions.length > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
for (String permission : permissions) {
if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
requestFinish = false;
}
}
}
grantedListener.onPermissionGrantedResult(requestFinish);
}
}
/**
* 权限授权结果委托
*/
public interface GrantedListener {
// 授予权限的结果,在对话结束后调用
void onPermissionGrantedResult(boolean permissionGranted);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionsUtils.handleRequestPermissionsResult(this, requestCode, permissions, (it -> {
if (!it) {
Toast.makeText(this, "权限获取失败", Toast.LENGTH_SHORT).show();
} else {
new Thread(this::addImage).start();
}
}));
}
以上onRequestPermissionsResul里调用工具类里的handleRequestPermissionsResult方法,判断是否获取成功,然后自行处理即可,这个是非常简略的一种方式,对于一些小Demo来说刚刚好,其他比较复杂的项目来说,还是建议使用诸如郭大神的权限库,更好用更全面。
图片加载
图片加载就是本篇文章的重点了,之前笔者对图片的三级缓存一问三不知,甚至概念都不清楚,直到入职了新公司,有个需求,需要用到这个三级缓存,在同事的指导下,终于直到了这玩意是个什么东西。(一点浅见)
图片加载的三级缓存,分别为内存缓存、磁盘缓存、网络缓存。众所周知网络缓存就是下载图片,频繁操作是非常浪费流量的,同时加载速度也不理想,特别是需要大量加载图片的场景。
三级缓存原理:
(1)需要加载某张图片时,先去内存缓存读取图片。
(2)若内存缓存中返回Null,就去磁盘中读取图片。
(3)若是磁盘缓存返回Null,就通过网络缓存下载图片,同时将图片存入内存缓存。
单例模式+线程池
大量图片的加载肯定是需要用到多线程的,而线程的创建或销毁会浪费大量性能,所以使用单例模式+线程池,一是减小性能开销,二是方便管理线程任务。先定义一个队列,用以表示图片加载任务。
private abstract static class BasePriorityRunnable implements Runnable, Comparable<BasePriorityRunnable> {
private final long priority;
BasePriorityRunnable(long priority) {
this.priority = priority;
}
@Override
public int compareTo(BasePriorityRunnable o) {
return (int) (o.priority - priority);
}
}
然后自定义设置线程池的核心线程,及任务队列数量等。
/**
* 核心线程数
*/
private static final int INIT_CORE_POOL_SIZE = 5;
/**
* 最大线程数
*/
private static final int MAX_POOL_SIZE = 15;
/**
* 最大存活时间
*/
private static final int MAX_KEEP_ALIVE_TIME = 5;
/**
* 任务队列
*/
private static final int MAX_TASK_QUEUE_LENGTH = 100;
private ExecutorService mExecutorService;
private void initThreadPool() {
if (mExecutorService != null) {
return;
}
try {
mExecutorService = new ThreadPoolExecutor(INIT_CORE_POOL_SIZE, MAX_POOL_SIZE, MAX_KEEP_ALIVE_TIME,
TimeUnit.SECONDS, new PriorityBlockingQueue<>(MAX_TASK_QUEUE_LENGTH, ((o1, o2) -> {
return ((BasePriorityRunnable) o1).compareTo((BasePriorityRunnable) o2);
})));
} catch (IllegalArgumentException | NullPointerException e) {
e.printStackTrace();
}
}
内存缓存
内存缓存我们使用LruCache,为最近最久使用算法,当容器满时将最久没被使用的数据移除容器。LruCache本质是一个HashMap封装而来,保证其线程安全。构造方法里必须传入一个容量,比如10x1024x1024,关于这个大小是有讲究的,容量太大,容易OOM,容量太小,又没有起到缓存的作用,我个人看法是使用将容量和最大内存相关(Runtime.getRuntime().maxMemory())。同时使用LruCache的时候必须重写sizeOf方法,返回其所占存储空间的大小。
另一方面,我们还需要重写其entryRemoved方法,用于当某个缓存被移除容器时,将该缓存存入本地内存。
/**
* 最大内存
*/
private final long max = Runtime.getRuntime().maxMemory() / 8;
private final HashMap<String, SoftReference<Bitmap>> lruMap = new HashMap<>();
private final LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>((int) max) {
@Override
protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
return value.getAllocationByteCount();
}
@Override
protected void entryRemoved(boolean evicted, @NonNull String key, @NonNull Bitmap oldValue, @Nullable Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
//从内存中移除时保存到本地内存
if (newValue != null) {
saveLocalBitmap(key, newValue);
}
}
};
private void setLru(String key, Bitmap bitmap) {
lruCache.put(key, bitmap);
}
private Bitmap getLru(String key) {
return lruCache.get(key);
}
本地缓存
这个本地缓存就是将Bitmap以文件形式存于内存中,取出的时候再通过文件转化为Bitmap。
private Context mContext;
private void saveLocalBitmap(String key, Bitmap bitmap) {
if (key == null || TextUtils.isEmpty(key) || bitmap == null || mContext == null) {
return;
}
checkFile(getCachePath(mContext), key);
String filePath = getCachePath(mContext) + key;
try {
FileOutputStream os = new FileOutputStream(filePath);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, os);
os.flush();
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private Bitmap getLocalBitmap(String key) {
if (key == null || TextUtils.isEmpty(key) || mContext == null) {
Log.e(TAG, "getLocalBitmap: 本地记录异常");
return null;
}
String filePath = getCachePath(mContext) + key;
try {
return BitmapFactory.decodeStream(new FileInputStream(new File(filePath)));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
网络缓存
网络缓存就是通过地址下载图片,下载图片的方式有很多,这里不多赘述,使用了一种简单的方式。
private Bitmap downloadBitmap(String url) {
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestMethod("GET");
int code = connection.getResponseCode();
if (code == 200) {
Bitmap bitmap = BitmapFactory.decodeStream(connection.getInputStream());
return compressBitmap(bitmap);
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "downloadBitmap: " + e.getMessage());
}
return null;
}
使用
使用的时候通过单例调用load方法即可,入参有两个,一个是本地地址或是网络地址,另外一个是回调,用于返回bitmap数据。
public void load(String path, Monitor<CacheData> result) {
initThreadPool();
mExecutorService.execute(new BasePriorityRunnable(System.currentTimeMillis()) {
@Override
public void run() {
loadBitmap(path, result);
}
});
}
private void loadBitmap(String path, Monitor<CacheData> result) {
String key = md5(path);
//从缓存拿
Bitmap cacheBitmap = getLru(key);
if (cacheBitmap != null) {
result.run(new CacheData(path, cacheBitmap));
return;
}
//从内存拿
Bitmap localBitmap = getLocalBitmap(key);
if (localBitmap != null) {
setLru(key, localBitmap);
result.run(new CacheData(path, localBitmap));
return;
}
Bitmap bitmap = null;
if (assessType(path) == NETWORK_TYPE) {
bitmap = downloadBitmap(path);
} else {
try {
bitmap = BitmapFactory.decodeStream(new FileInputStream(new File(path)));
bitmap = compressBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
if (bitmap != null) {
setLru(key, bitmap);
result.run(new CacheData(path, bitmap));
}
}
自定义方形图片
通过笔者粗略的调研,发现方形图片只需要自定义View重写onMeasure方法即可,返回宽高一致,当然这很粗暴且不优雅。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(width, width);
}
使用Recyclerview展示图片
这个其实也很简单,自定义RecyclerView.Adapter即可。
这里重点展示图片加载的部分,首先给image设置一个tag,tag可以是图片的key即地址,在调用图片加载工具里的load方法。其实写到这里,突然觉得这个工具类好像写得并不怎么好,权当给自己一个记录了。我个人也推荐Glid图片加载,好用简单且稳定。
holder.image.setTag(item);
CacheUtils.getInstance().load(item,it->{
holder.image.post(()->{
if (it == null || holder.image.getTag() != it.getKey()){
holder.image.setImageResource(R.mipmap.test);
}else {
holder.image.setImageBitmap(it.getBitmap());
}
});
return false;
});
结语
本文的重点就是图片加载,其余的我觉得都不重要。所有代码均可在我的git仓库里找到,就不在这里多多赘述了,后续关于排行榜、图片自定义切割这部分功能,随缘吧,暂时没时间没精力去写。
需要源码的朋友欢迎访问:拼图