1.首先看一下效果
1.1做成了一个GIF
1.2.我用格式工厂有点问题,大小无法调到手机这样的大小,目前还没有解决方案。
1.3.网上有免费的MP4->GIF,参考一下这个网站吧。
1.4.讲解一下这个图片吧,首先是从话题中点击了其中一张图片,进入图片Activity,
然后可以自由放大,自由翻转。
2.分析一下继承的BaseImageActivity
2.1因为ImageActivity继承了BaseImageActivtiy,首先看看源代码。
/* * Copyright 2017 GcsSloop * * 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. * * Last modified 2017-03-15 20:02:59 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.base.app; import android.content.Intent; import java.util.ArrayList; /** * 对数据进行预处理 */ public abstract class BaseImageActivity extends BaseActivity { public static final String ALL_IMAGE_URLS = "all_images"; public static final String CURRENT_IMAGE_URL = "current_image"; protected static final String MODE_NORMAL = "normal"; protected static final String MODE_ERROR = "error"; protected String mCurrentMode = MODE_NORMAL; protected ArrayList<String> images = new ArrayList<>(); // 所有图片 protected String current_image_url = null; // 当前图片 protected int current_image_position = 0; // 当前图片位置 @Override protected void initDatas() { super.initDatas(); // 初始化图片 url 和 图片集合,保证两个数据都存在 Intent intent = getIntent(); // 没有传递当前图片,设置模式为错误 String imageUrl = intent.getStringExtra(CURRENT_IMAGE_URL); if (imageUrl == null || imageUrl.isEmpty()) { toastShort("没有传递图片链接"); mCurrentMode = MODE_ERROR; return; } mCurrentMode = MODE_NORMAL; ArrayList<String> temp = intent.getStringArrayListExtra(ALL_IMAGE_URLS); if (temp == null || temp.size() <= 0) { // 记录当前图片,计算位置 images.clear(); images.add(imageUrl); } else if (temp.size() > 0) { // 如果图片集合大于0 images = new ArrayList<>(temp); } if (!images.contains(imageUrl)) { images.add(imageUrl); } current_image_url = imageUrl; current_image_position = images.indexOf(current_image_url); } }
2.2这里需要回顾一下BaseActivity。
BaseActivity==>主要提供了项目本身API提供的Diycode类,ViewHolder类,Toast类,两个抽象方法。ActionBar。结束
当前活动。
打开活动。
2.3.私有成员变量。
·存放图片链接的一个ArrayList<String>
·存放当前图片的链接current_image_url
·存放当前图片位置,整型current_image_positon
2.4.注意点,Override一个在基类中已经实现过的函数。
重载,也是可以Override的。以前理解的Override就是抽象方法,接口方法。这里又涨知识了。
如果写了一个父类中有的方法,就相当于重载了父类中的方法,就不会执行父类中的那个方法,而是执行自己的方法了。
名字是一样的。
2.5.在BaseActivity中写了一个空的initDatas方法。而且规定了这个方法执行的顺序。
然后在子类BaseImageActivity中重载这个方法。
这个类主要处理图片数据的。
2.6分析这个重载方法。
2.6.1首先从intent中获得当前图片,如果没有,设置模式为错误。否则,设置模式为正常。
2.6.2然后从intent中获得所有图片集合,如果没有,就把当前图片加进images去,如果有,就将集合加到一个images中。
2.6.3然后它还判断了一下images中是否包含了当前url,应该是双保险吧,感觉没必要。
2.6.4最后,就把当前图片和当前位置更新了一下。
3.分析一下图片缓存DiskImageCache
3.1首先看看在ImageActivity中定义的一个变量。DiskImageCache mCache;源代码如下:
/* * Copyright 2017 GcsSloop * * 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. * * Last modified 2017-03-12 00:56:52 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.base.webview; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Environment; import android.os.StatFs; import android.support.annotation.NonNull; import android.util.Log; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Comparator; public class DiskImageCache { private static final String CACHE_SUFFIX = ".cache"; private static final int MB = 1024 * 1024; private static final int CACHE_SIZE = 50; // 缓存占用空间大小 private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 10; // 为 SD 卡保留多少空间 private File cacheDir; public DiskImageCache(Context context) { cacheDir = getDiskCacheDir(context, "web-image"); // 整理缓存 organizeCache(cacheDir); } /** * 从缓存中获取图片 **/ public Bitmap getBitmap(final String key) { final String path = getCachePath(key); File file = new File(path); if (file.exists()) { Bitmap bmp = BitmapFactory.decodeFile(path); if (bmp == null) { file.delete(); } else { updateFileTime(path); return bmp; } } return null; } /** * 将图片存入文件缓存 **/ public void saveBitmap(String key, Bitmap bm) { if (bm == null) { return; } //判断sdcard上的空间 if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { return; //SD空间不足 } File file = new File(getCachePath(key)); try { file.createNewFile(); OutputStream outStream = new FileOutputStream(file); bm.compress(Bitmap.CompressFormat.PNG, 100, outStream); outStream.flush(); outStream.close(); } catch (FileNotFoundException e) { Log.w("ImageFileCache", "FileNotFoundException"); } catch (IOException e) { Log.w("ImageFileCache", "IOException"); } } /** * 保存 bytes 数据 * * @param key url * @param bytes bytes 数据 */ public void saveBytes(String key, byte[] bytes) { if (bytes == null) { return; } //判断sdcard上的空间 if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { return; //SD空间不足 } File file = new File(getCachePath(key)); try { file.createNewFile(); OutputStream outStream = new FileOutputStream(file); outStream.write(bytes); outStream.flush(); outStream.close(); } catch (FileNotFoundException e) { Log.w("ImageFileCache", "FileNotFoundException"); } catch (IOException e) { Log.w("ImageFileCache", "IOException"); } } /** * 获取一个本地缓存的输入流 * * @param key url * @return FileInputStream */ public FileInputStream getStream(String key) { File file = new File(getCachePath(key)); if (!file.exists()) return null; try { FileInputStream inputStream = new FileInputStream(file); return inputStream; } catch (FileNotFoundException e) { Log.e("getStream", "FileNotFoundException"); e.printStackTrace(); } return null; } /** * 获取本地缓存路径 * * @param key url * @return 路径 */ public String getDiskPath(String key) { File file = new File(getCachePath(key)); if (!file.exists()) return null; return file.getAbsolutePath(); } /** * 是否有缓存 * * @param key url * @return 是否有缓存 */ public boolean hasCache(String key) { File file = new File(getCachePath(key)); return file.exists(); } /** * 计算存储目录下的文件大小, * 当文件总大小大于规定的CACHE_SIZE或者sdcard剩余空间小于FREE_SD_SPACE_NEEDED_TO_CACHE的规定 * 那么删除40%最近没有被使用的文件 */ private boolean organizeCache(@NonNull File cacheDir) { File[] files = cacheDir.listFiles(); if (files == null) { return true; } if (!Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { return false; } int dirSize = 0; for (int i = 0; i < files.length; i++) { if (files[i].getName().contains(CACHE_SUFFIX)) { dirSize += files[i].length(); } } if (dirSize > CACHE_SIZE * MB || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { int removeFactor = (int) ((0.4 * files.length) + 1); Arrays.sort(files, new FileLastModifSort()); for (int i = 0; i < removeFactor; i++) { if (files[i].getName().contains(CACHE_SUFFIX)) { files[i].delete(); } } } if (freeSpaceOnSd() <= CACHE_SIZE) { return false; } return true; } /** * 修改文件的最后修改时间 **/ public void updateFileTime(String path) { File file = new File(path); long newModifiedTime = System.currentTimeMillis(); file.setLastModified(newModifiedTime); } /** * 计算sdcard上的剩余空间 **/ private int freeSpaceOnSd() { StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath()); double sdFreeMB = ((double) stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB; return (int) sdFreeMB; } /** * 根据文件的最后修改时间进行排序 */ private class FileLastModifSort implements Comparator<File> { public int compare(File arg0, File arg1) { if (arg0.lastModified() > arg1.lastModified()) { return 1; } else if (arg0.lastModified() == arg1.lastModified()) { return 0; } else { return -1; } } } /** * 获取缓存文件的绝对路径 */ private String getCachePath(String key) { return cacheDir.getAbsolutePath() + File.separator + convertKey(key); } /** * 获取磁盘缓存文件夹 优先获取外置磁盘 * * @param context 上下文 * @param uniqueName 自定义名字 */ public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } File cacheDir = new File(cachePath + File.separator + uniqueName); if (!cacheDir.exists()) cacheDir.mkdir(); return cacheDir; } /** * 哈希编码 */ public String convertKey(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey + CACHE_SUFFIX; } private String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); } }
3.2定义的常量及变量预览。
3.3.构造函数就一个。实现方法在后面。
3.4.如何从缓存中获取图片(通过一个String返回一个Bitmap),这个项目暂时没有用到。
3.5.如何将图片存入文件缓存?(将一个bitmap,存放一个缓存路径为一个key下)
3.6.如何保存bytes数据?(也是存放到一个key中)
3.7.如何获取一个本地缓存的输入流?(通过一个key获得FileInputStream)
3.8.如何获取本地缓存路径?(通过一个key得到一个String)
3.9.如何判断是否有缓存?(通过一个key返回boolean)
3.10.然后是关键的整理存储空间了。这个删除文件很关键,不然数据会越来越大的。
3.11.如何修改文件的最后修改时间?(参数为文件path)
3.12.如何计算sdcard上的剩余空间?
3.13.如何根据文件的最后修改时间进行排序?(继承了一个Comparator<File>的内部类)
FileLastModifSort其实就是一个类,类中有一个比较的函数,有两个File的参数,比较哪个更加最后而已。
3.14.如何获取缓存文件的绝对路径?(通过key得到一个String)
3.15.如何获取磁盘缓存文件夹,优先获取外置磁盘?(参数为context和一个string返回一个File)
3.16.什么是哈希编码?(参数为key返回一个String)
通过MD5加密后加了一个后缀为.cache,返回这个字符串。
3.17.bytes数组转化为哈希String?(参数为byte[]返回一个String)
这个在3.16.哈希编码中用到了,就是系统函数很多会转化为bytes,这个函数就是将byte[]类型的数据整合成String,
然后再处理逻辑。
这个DiskImageCache比较长,主要处理的就是一些缓存问题了,图片缓存,和本地的存储器挂钩了,所以就有很多函数
要调用。
4.回到ImageActivity
4.1这里首先复写了一个getLayoutId,返回一个资源id,其实就是ImageActivity布局
贴一下activity_image源代码。
<?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright 2017 GcsSloop ~ ~ 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. ~ ~ Last modified 2017-03-15 23:45:17 ~ ~ GitHub: https://github.com/GcsSloop ~ Website: http://www.gcssloop.com ~ Weibo: http://weibo.com/GcsSloop --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_image" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.gcssloop.diycode.activity.ImageActivity"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content"/> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v4.view.ViewPager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="match_parent"> </android.support.v4.view.ViewPager> <com.rd.PageIndicatorView android:id="@+id/pageIndicator" android:layout_marginBottom="16dp" android:layout_centerHorizontal="true" android:layout_alignParentBottom="true" android:layout_width="wrap_content" android:layout_height="wrap_content" app:piv_viewPager="@id/view_pager" app:piv_animationType="worm" app:piv_autoVisibility="false" app:piv_radius="4dp" app:piv_padding="12dp" app:piv_unselectedColor="@color/diy_gray2" app:piv_selectedColor="@color/diy_red" /> </RelativeLayout> </LinearLayout>
4.2分析一下布局代码的结构
4.3.和真实页面对比一下。
4.4.这里有一个新的知识点==>PageIndicatorView
这是一个开源框架。挺不错的。简单实用。
作用就是当页面在切换时,一个提示作用,下面的小红点会告诉用户这是第几张图或者第几个页面。
4.5.然后是复写一个initViews方法。
4.5.1.注意,这个函数在initDatas之后调用。initDatas()函数在BaseImageActivity中调用了。
4.5.2.这里面通过setTitle设置了标题栏的查看图片。
4.5.3.这里从参数ViewHolder中获得了布局中的ViewPager,显然ViewHolder中BaseActivity中定义
然后ViewHolder初始化其实也是在BaseActivity中进行的,利用了一个抽象函数getLayoutId()
然后initData(),然后initViews(),所以这个ImageActivity才能直接获得这个ViewPager的Id。
4.5.4.这里看一下initViews中的部分代码。
对于getLayoutInflater不太理解,参考一下这篇文章。
4.5.4.1.它的作用类似于findViewById()。
4.5.4.2.LayoutInflater是用来找res/layout/下的xml布局文件,并且实例化。
4.5.4.3.FindViewById是找xml布局文件下的具体widget控件(如Button、TextView等)
4.5.5.这里继续initViews中的部分代码。
4.5.5.1.这里是在ImageActivity中定义的布局文件中的viewPager设置适配器的过程。
4.5.5.2.首先是复写了getCount方法,直接返回图片的张数。
4.5.5.3.然后是复写了isViewFromObject方法,两个参数(View,Object),使两者相等。
4.5.5.4.然后复写了一个关键的函数,instantiateItem(ViewGroup,int)
4.5.5.6.这里不得不提一下的是PhotoView。
也是第三方开源库,支持双指缩放,旋转。
看一下效果图吧!
4.5.5.7.分析一下instantiateItem==>这个方法返回一个对象,这个对象表明了PagerAdapter适配器选
择那个对象放在当前的ViewPager中。
然后这里还处理了缓存,如果缓存中有,就利用Glide.with(context).load(file).into()
如果缓存中没有,就利用Glide.with(context).load(图片url).into()方法
然后将这个photoView添加到容器中,这个容器就是instantiateItem中的第一个参数。
4.5.5.8.分析一下复写的destroyItem()方法,3个参数(ViewGroup,int,Object)
这个方法,是从ViewGroup中移除当前View。
4.5.5.9.然后将viewPager确定到第几项。
调用了基类BaseImageActivity中的静态常量current_image_position。
这样可以确定指示剂的位置。
4.6.然后回到ImageActivity中,最后一个函数是loadImage(string,photoView)
作用:给一个url和一个photoView,加载一张图到photoView中。
这里用到了一个第三方库。图片加载库。
源码如下:
这个应该是设置了缓存策略。
5.总结一下
5.1.首先理解一下哪里会用到ImageActivity这个活动,这个活动不是局限于某一个模块,而是所有地方,只要包含了图片
的地方,都是有可能调用intent来跳转到这里的。所以这个有点像之前做的大学喵,点击图片查看大图,支持缩放。
5.2.然后理解一下为什么这个ImageActivity要继承一个BaseImageActivity,这个BaseImageActivity实际上是一个抽象类,
定义了所有图片数据,所以真实数据在这里面处理,确定当前图片url,当前是第几个图片。
5.3.然后定义了一个DiskImageCache,主要是处理图片缓存的,基本上每个项目都会涉及到图片缓存,而且这些方法都是
必须实现而且基本一样,所以今后的项目也可以参考一下这个类,封装好的,耦合很低。
5.4.然后是一个imageActivity的布局,通过实现了BaseActivity中的抽象函数getLayoutId,来获得这个布局的资源id。
然后在initViews中的参数holder中,利用holder.get(R.id.view_pager)就能访问到这个ViewPager。
5.5.初始化view,注意是在initDatas调用之后,因为有了数据,才能填充到真实的视图。这里可以setTilte()来设置标题。
然后就是一些缓存处理了。
5.6.真正的图片要加载处理,必须要有一个adapter,所以这里new了一个PagerAdapter,实现了4个必须实现的方法。主要
逻辑在instantiateItem(ViewGroup container,int position)中处理,比如开启photoView缩放功能。以及缓存处理。
5.7.initViews函数执行结束之前一定要设置这个viewPager当前的页面,调用setCurrentItem(int)即可。
这个int其实是在父类中定义,因为这个是继承了父类,也可以直接调用父类中的变量。
5.8.最后是一个图片缓存策略,因为是灰色的,也没有执行,可以也不是必须要实现的。但是要是不得不说Glide有点强大,
特别是处理图片问题,图片缓存问题,图片加载问题。有空再深究一下。