Android小游戏从零开始——拼图(2):自由选择图片

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仓库里找到,就不在这里多多赘述了,后续关于排行榜、图片自定义切割这部分功能,随缘吧,暂时没时间没精力去写。

需要源码的朋友欢迎访问:拼图

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值