Diycode开源项目 ImageActivity分析

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);
    }
}
View Code

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();
    }

}
View Code

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>
View Code

 

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。

        也是第三方开源库,支持双指缩放,旋转。

        使用方法点击这里

        github原地址点击这里。

        看一下效果图吧!

        

 

   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中。

      这里用到了一个第三方库。图片加载库。

      Glide用法详解点击这里。

      Glide的原地址点击这里。

      源码如下:

      

      这个应该是设置了缓存策略。

      

      


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有点强大,

  特别是处理图片问题,图片缓存问题,图片加载问题。有空再深究一下。


posted @ 2017-11-16 12:52  Jason_Jan  阅读(508)  评论(0编辑  收藏  举报