Android-仿微信图片选择器

视频教程原址:http://www.imooc.com/learn/489

这次,小编我学习了一个仿照微信的图片选择器的实例,接下来,就将贴上我的code,当然,里面的注解有的是根据老师的原话总结的,不过更多的,是根据我自己的理解


首先,先将这次实例的关键类先贴出来:ImageLoader
关于这个类的作用呢,是用来加载图片的,下面是该类要实现的目标

1、尽可能的去避免内存溢出
   a、根据图片的显示大小去压缩图片
   b、使用缓存对我们的图片进行管理(LruCache)

2、用户操作UI控件必须充分的流畅
适配器的getView方法里面尽可能不去做耗时的操作(异步加载+回调显示)

3、用户预期显示的图片尽可能的快(图片的加载策略选择 LIFO后进先出)

接着是关于该类的实现流程(因为该类是在适配器中的getView方法中调用的):

getView() {
   url -> Bitmap(在getView要实现的是根据url得到bitmap然后设置给GridView中的item)
   (下面的是在ImageLoader大致实现:
   url -> LruCache查找(LruCache用来缓存图片)
         -> 找到返回,利用一个Handler设置回调为ImageView设置图片
         -> 找不到 url -> 创建一个Task -> 将Task放入TaskQueue且发送一个通知去提醒后台轮询线程

               Task -> run() {
                根据url加载图片
                1、获得图片显示的大小
                2、使用Options对图片进行压缩
                3、加载图片且放入LruCache
               }
   )
}

关于后台轮询线程:
从TaskQueue -> 取出Task -> 交给线程池去执行
采用Handler+Looper+Message(Android异步执行处理框架)去实现
(Looper主要作用:
1、与当前线程绑定,保证一个线程只会有一个Looper实例,同时一个Looper实例也只有一个MessageQueue。
2、loop()方法,不断从MessageQueue中去取消息,交给消息的target属性的dispatchMessage去处理。
好了,我们的异步消息处理线程已经有了消息队列(MessageQueue),也有了在无限循环体中取出消息的哥们,现在缺的就是发送消息的对象了,于是乎:Handler登场了。)

启发:安卓中Handler如果在某个线程中初始化,则属于该线程(切记需要在初始化前需要调用Looper.prepare(),初始化后需要调用Looper.loop()启动消息队列)。

import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.LruCache;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;

/**
 * 图片加载类 (采用单例模式)
 * 
 * @author Just
 * 
 */
public class ImageLoader {

    private static ImageLoader mInstance;

    // 成员变量
    /**
     * 图片缓存的核心对象
     */
    private LruCache<String, Bitmap> mLruCache;// String表示图片的路径
    /**
     * 线程池,用于统一处理Task (ImageLoader中的存在一个后台线程来取任务然后加入到线程池中)
     */
    private ExecutorService mThreadPool;
    private static final int DEAFULT_THREAD_COUNT = 1;// 线程池的默认线程数
    /**
     * 队列的调度方式 (即用来记录图片的加载策略,默认为LIFO)
     */
    private Type mType = Type.LIFO;
    /**
     * 任务队列,供线程池取任务
     */
    private LinkedList<Runnable> mTaskQueue;
    /**
     * 后台轮询线程
     */
    private Thread mPoolThread;
    private Handler mPoolThreadHandler;// 是与上面定义的线程绑定在一起的,用于给线程发送消息
    /**
     * UI线程中的Handler 用于传入一个Task以后,当图片获取成功以后,
     * 用mUIHandler发送消息,为图片设置回调(回调显示图片的Bitmap)
     */
    private Handler mUIHandler;

    /**
     * Semaphore(信号量)通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
     * 通过信号量控制mPoolThread(后台轮询线程)中初始化mPoolThreadHandler与
     * 使用的loadImage(loadImage中的调用了addTask(addTask中使用了mPoolThraedhandler))的线程的顺序
     */
    private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);// 初始化时设置可用的许可证数量为0,使得当addTask在mPoolThreadHandler初始化之前执行时处于等待状态

    private Semaphore mSemaphoreThreadPool;// 再init()中根据线程数量指定信号量并初始化

    public enum Type {
        FIFO, LIFO;
    }

    private ImageLoader(int mThreadCount, Type type) {// 可以由用户决定线程池的线程数以及队列的调度方式
        init(mThreadCount, type);
    }

    /**
     * 初始化
     * @param mThreadCount
     * @param type
     */
    private void init(int mThreadCount, Type type) {

        // 后台轮询线程,这里涉及的Looper可以阅读这篇博客 [http://blog.csdn.net/lmj623565791/article/details/38377229]
        mPoolThread = new Thread() {
            @Override
            public void run() {
                Looper.prepare();//Looper.prepare()是在后台轮询线程中调用的
                // Looper用于封装了android线程中的消息循环,默认情况下一个线程是不存在消息循环(message loop)的,
                // 需要调用Looper.prepare()来给线程创建一个消息循环,调用Looper.loop()来使消息循环起作用,
                // 从消息队列里取消息,处理消息。


                // "找不到 url -> 创建一个Task -> 将Task放入TaskQueue且发送一个通知去提醒后台轮询线程"
                // 如果来了一个任务,mPoolThreadHandler会发送一个Message到Looper中,最终会调用handleMessage
                mPoolThreadHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        // 线程池取出任务进行执行
                        mThreadPool.execute(getTask());

                        try {
                            mSemaphoreThreadPool.acquire();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                // 发布一个许可证,从而可以唤醒addTask的等待
                mSemaphorePoolThreadHandler.release();
                Looper.loop();
            }
        };

        mPoolThread.start();

        int maxMemory = (int) Runtime.getRuntime().maxMemory();// 获取应用最大可用内存
        int cacheMemory = maxMemory / 8;// 用于初始化mLruCache
        mLruCache = new LruCache<String, Bitmap>(cacheMemory) {
            @Override
            protected int sizeOf(String key, Bitmap value) {// 测量每个Bitmap所占据的内存并返回
                return value.getRowBytes() * value.getHeight();// 每行占据的字节数*高度
            }
        };

        // 创建线程池
        mThreadPool = Executors.newFixedThreadPool(mThreadCount);
        mTaskQueue = new LinkedList<Runnable>();
        mType = type;

        mSemaphoreThreadPool = new Semaphore(mThreadCount);
    }

    /**
     * 从任务队列取出一个Runnable
     * 
     * @return
     */
    private Runnable getTask() {
        if (mType == Type.FIFO) {
            return mTaskQueue.removeFirst();
        } else if (mType == Type.LIFO) {
            return mTaskQueue.removeLast();
        }
        return null;
    }

    public static ImageLoader getInstance() {
        // 外层的if判断不做同步处理,但是可以过滤掉大部分的代码,当mInstance初始化后基本上if体里面的代码就不要执行了
        // 但是在刚开始mInstance未初始化的时候,因未作同步的处理,可能会有一两个线程同时到达if体里面
        // 等到达里面后再做synchronized处理,这样会使得需要做同步的线程减少,只有一开始的几个
        if (mInstance == null) {
            // 就是这个点可能会有几个线程同时到达
            synchronized (ImageLoader.class) {
                // 里层的if判断是必要的,因为在外层if判断之后的那个点可能会有几个线程同时到达,
                // 接着在进行synchronized,如果不加判断可能会new出几个实例
                if (mInstance == null)
                    mInstance = new ImageLoader(DEAFULT_THREAD_COUNT, Type.LIFO);
            }
        }

        // 上面的这种处理相比于public synchronized static ImageLoader getInstance()提高了效率

        return mInstance;
    }

    public static ImageLoader getInstance(int threadCount, Type type) {
        if (mInstance == null) {
            synchronized (ImageLoader.class) {
                if (mInstance == null)
                    mInstance = new ImageLoader(threadCount, type);
            }
        }

        return mInstance;
    }

    /**
     * 根据path为imageView设置图片
     * 
     * @param path
     * @param imageView
     */
    public void loadImage(final String path, final ImageView imageView) {
        imageView.setTag(path);// 防止item复用的时候,imageView复用造成图片错乱,imageView设置图片时会根据Tag对比path
        //这里可以参考一下[http://blog.csdn.net/lmj623565791/article/details/24333277]这篇博客

        if (mUIHandler == null) {
            mUIHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    // 获取得到的图片,为imageView回调设置图片
                    ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
                    Bitmap b = holder.bitmap;
                    ImageView iv = holder.imageView;
                    String p = holder.path;
                    // 将path与getTag存储路径进行比较

                    if (iv.getTag().toString().equals(p)) {
                        iv.setImageBitmap(b);
                    }
                }
            };
        }

        // 根据path在缓存中获取bitmap
        Bitmap bm = getBitmapFromLruCache(path);
        if (bm != null) {
            refreashBitmap(path, imageView, bm);
        } else {
            // Task的任务就是压缩图片,然后将图片加入到缓存之中,然后回调图片(即将图片载入到指定的imageView中)
            addTask(new Runnable() {
                @Override
                public void run() {
                    // 加载图片,涉及图片的压缩
                    // 1、获得图片需要显示的大小(即刚好为imageView的大小)
                    ImageSize imageSize = getImageViewSize(imageView);
                    // 2、压缩图片
                    Bitmap b = decodeSampledBitmapFromPath(path,
                            imageSize.width, imageSize.height);
                    // 将图片加入到缓存中
                    addBitmapToLruCache(path, b);

                    refreashBitmap(path, imageView, b);

                    mSemaphoreThreadPool.release();// 当任务一旦执行完,就释放一个许可证
                }
            });
        }
    }

    private void refreashBitmap(final String path, final ImageView imageView,
            Bitmap b) {
        Message message = Message.obtain();// 从整个Messge池中返回一个新的Message实例,避免分配新的对象,减少内存开销
        ImgBeanHolder holder = new ImgBeanHolder();
        holder.bitmap = b;
        holder.path = path;// loadImage的形参
        holder.imageView = imageView;// loadImage的形参
        message.obj = holder;
        mUIHandler.sendMessage(message);
    }

    /**
     * 将图片加入到LruCache
     * 
     * @param path
     * @param b
     */
    private void addBitmapToLruCache(String path, Bitmap b) {
        if (getBitmapFromLruCache(path) == null) {// 需要判断缓存中是否已经存在
            if (b != null) {
                mLruCache.put(path, b);
            }
        }
    }

    /**
     * 根据图片需要的显示的宽和高进行压缩
     * 
     * @param path
     * @param width
     * @param height
     * @return
     */
    private Bitmap decodeSampledBitmapFromPath(String path, int width,
            int height) {
        // 获取图片实际的宽和高,并不把图片加载到内存中
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;// 使得图片不加载到内存中,但是options.outWidth和options.outHeight会存在值
        BitmapFactory.decodeFile(path, options);// options中会包含图片实际的宽和高(即options.outWidth和options.outHeight)
        options.inSampleSize = caculateInSampleSize(options, width, height);

        // 使用获取到的inSampleSize再次解析图片并且加载到内存中
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFile(path, options);

        return bitmap;
    }

    /**
     * 根据需求的宽和高以及实际的宽和高计算SampleSize(压缩比例)
     * 
     * @param options
     * @param width
     * @param height
     * @return
     */
    private int caculateInSampleSize(Options options, int reqWidth,
            int reqHeight) {
        int width = options.outWidth;
        int height = options.outHeight;

        int inSampleSize = 1;

        // 这儿只是一个普通的压缩策略,当然也可以自己定制特别的压缩策略
        if (width > reqWidth || height > reqHeight) {
            int widthRadio = Math.round(width * 1.0f / reqWidth);// 将比例四舍五入
            int heightRadio = Math.round(height * 1.0f / reqHeight);

            inSampleSize = Math.max(widthRadio, heightRadio);// 为了图片不失帧,保持原比例一般取小值,但是得到的图片会始终比显示区域大一些,这里为了节省内存,所以尽量的压缩
        }

        return inSampleSize;
    }

    /**
     * 根据ImageVIew获取适当的压缩的宽和高
     * 
     * @param imageView
     * @return
     */
    // @SuppressLint("NewApi")
    // 在Android代码中,我们有时会使用比我们在AndroidManifest中设置的android:minSdkVersion版本更高的方法,
    // 此时编译器会提示警告,解决方法是在方法上加上@SuppressLint("NewApi")或者@TargetApi()
    // SuppressLint("NewApi")作用仅仅是屏蔽android lint错误,所以在方法中还要判断版本做不同的操作
    private ImageSize getImageViewSize(ImageView imageView) {
        ImageSize imageSize = new ImageSize();

        DisplayMetrics displayMetrics = imageView.getContext().getResources()
                .getDisplayMetrics();

        // imageView在布局中的大小可能是固定大小的,也有可能是相对的

        LayoutParams lp = imageView.getLayoutParams();

        int width = imageView.getWidth();// 获取imageView的实际宽度
        // 有可能ImageView刚被new出来而没有添加到容器中,或者其他原因,导致无法获取真正的width,因此需要判断一下
        if (width <= 0) {
            width = lp.width;// 设置为在布局中声明的宽度,但是有可能在布局中是wrap_content(-1),match_parent(-2),所以需要下一步的判断
        }
        if (width <= 0) {
            // 赋值为最大值,但是如果没有设置的话,width依然是获取不到的理想的值的,因此还需要下一步判断

            // getMaxWidth方法是在API16中才有,还需要处理一下,利用反射获取,以便兼容到16以下的版本
            // width=imageView.getMaxWidth();
            width = getImageViewFieldValue(imageView, "mMaxWidth");// "mMaxWidth"可以去ImageView的源码中查看
        }
        if (width <= 0) {
            width = displayMetrics.widthPixels;// 最后没办法,只能等于屏幕的宽度
        }

        int height = imageView.getHeight();
        if (height <= 0) {
            height = lp.height;
        }
        if (height <= 0) {
            // height=imageView.getMaxHeight();
            height = getImageViewFieldValue(imageView, "mMaxHeight");
        }
        if (height <= 0) {
            height = displayMetrics.heightPixels;
        }

        imageSize.width = width;
        imageSize.height = height;

        return imageSize;
    }

    /**
     * 通过反射获取ImageView的某个属性值
     * 
     * @param object
     * @param fieldName
     * @return
     */
    public static int getImageViewFieldValue(ImageView object, String fieldName) {
        int value = 0;

        try {
            Field field = ImageView.class.getDeclaredField(fieldName);
            // Field 提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。
            // getDeclaredField返回指定字段的字段对象

            field.setAccessible(true);// 在反射使用中,如果字段是私有的,那么必须要对这个字段设置

            int fieldValue = field.getInt(object);// 因为包括指定类的所有实例的指定字段(除静态字段),所以在这里需要指定实例对象

            if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {
                value = fieldValue;
            }

        } catch (Exception e) {
        }

        return value;
    }

    /**
     * addTask需要同步(synchronized),避免多个线程造成mSemaphorePoolThreadHandler.acquire()
     * 从而导致死锁的状态 且mTaskQueue.add(runnable)本身也需要同步
     * 
     * @param runnable
     */
    private synchronized void addTask(Runnable runnable) {
        mTaskQueue.add(runnable);

        try {
            if (mPoolThreadHandler == null)
                mSemaphorePoolThreadHandler.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        mPoolThreadHandler.sendEmptyMessage(0x110);
    }

    /**
     * 根据path在缓存中获取bitmap
     * 
     * @param key
     * @return
     */
    private Bitmap getBitmapFromLruCache(String key) {
        return mLruCache.get(key);
    }

    private class ImageSize {
        public int width;
        public int height;
    }

    private class ImgBeanHolder {
        public Bitmap bitmap;
        public ImageView imageView;
        public String path;
    }
}

下面是关于这段代码需要注意的几点(也许我根据视频原话的理解有错误,欢迎指正):
视频3-1的4:40起
在mUIHandler中的handleMessage中不能直接用msg.obj来取bitmap(前提是在sendMessage的时候只在msg.obj中绑定bitmap)然后根据loadImage方法参数path和imageView来设置图片,因为这里有一个重要的前提,那就是ImageLoader采用的是单例模式,在整个程序中只有一个实例,所以会导致mUIHandler中的handleMessage在主线程中执行的时候所获得的bitmap不一定是当前的(与之对应的)path和imageView(也就是当前loadImage方法的参数path和imageView,因为mUIHandler是在loadImage中初始化的,所以mUIHandler的handleMessage可以直接调用loadImage方法的参数path和imageView),因为我们会根据path用getBitmapFromLruCache方法在缓存中获取图片,或者异步去加载(开启新的线程去加载),加载完成后会回调mUIHandler中的handleMessage。
在显示GridView的时候,适配器的getView方法会被多次调用,因此loadImage也会被多次调用,而每次调用loadImage都会通过refreashBitmap方法来mUIHandler.sendMessage,而Task中refreashBitmap会新的线程中去执行,因此也许会有多个线程并发执行,也许当执行某一个loadImage的时候,正好mUIHandler的handleMessage也刚好在处理一个非该loadImage而sendMessage的message(注意:mUIHandler的handleMessage是在主线程中),如果不用ImgBeanHolder,就可能导致用msg.obj来取bitmap且调用刚才说的loadImage的参数的path和imageView而发生错乱
解决方法:设置一个内部类ImgBeanHolder,将对应的imageView,bitmap和path绑定成ImgBeanHolder实例的成员

视频3-4的3:55起
后台轮询的线程的执行理论上和loadImage方法的执行理论上应该是并行的
但是实际中可能在使用mPoolThraedhandler的时候mPoolThraedhandler还没有初始化
当用户new一个ImageLoader的以后有可能会直接调用loadImage方法,而loadImage中的调用了addTask(addTask中使用了mPoolThraedhandler)
解决方法:利用信号量,mSemaphorePoolThreadHandler
总结:当一个类中使用了两个线程,且一个线程使用的另外一个线程的变量时,一定要保证该变量已经初始化了

视频3-5起
Task -> TaskQueue -> 通知后台线程池取线程 -> 线程池把Task取出放入到自己内部的任务队列中(这里只是放到自己的任务队列中,至于要多久才能完成任务就没有管了,即使没有执行完,如果收到了通知,还是会从TaskQueue取任务)
(这一系列到放入线程池的内部队列为止的执行都是瞬间的,从而导致mTaskQueue中始终不大于一个任务,使得FIFO或LIFO没有区别)
修改:使得只有当目前的Task执行完成后才会去取新的Task
解决方法:利用信号量,mSemaphoreThreadPool

视频3-6起
在getImageViewSize方法中用到了一个API 16的方法getMaxWidth以及getMaxHeight
解决方法:利用反射获取 自定义方法 public static int getImageViewFieldValue();

还有我这里将要导的包也贴出来,是希望大家注意别导错包了,不然有些实例的方法是实现不了的


然后是主布局中的GridView所要用到的适配器:ImageAdapter

public class ImageAdapter extends BaseAdapter {

    /**
     * 记录图片是否被选中
     * 当改变文件夹的时候,会重新new一个ImageAdapter
     * 所以在这里用static,在不同的对象间共享数据
     * 使得即使是改变了文件夹,当回到原来的文件夹时,已经被选中的图片不会被取消
     */
    private static Set<String> mSelectedImg=new HashSet<String>();

    private String mDirPath;
    private List<String> mImagePaths;
    private LayoutInflater mInflater;//用于加载item的布局

    private int mScreenWidth;//屏幕的宽度

    /**
     * @param context
     * @param mDatas:传入的文件夹下所有图片的文件名
     * @param dirPath:图片所在的文件夹的路径
     * List存储的是图片的文件名而非图片的路径,如果图片比较多,存储路径的话浪费内存
     */
    public ImageAdapter(Context context,List<String> mDatas,String dirPath) {       
        this.mDirPath=dirPath;
        mImagePaths=mDatas;
        mInflater=LayoutInflater.from(context);

        WindowManager wm=(WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics=new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        mScreenWidth=outMetrics.widthPixels;
    }

    @Override
    public int getCount() {
        return mImagePaths.size();
    }

    @Override
    public Object getItem(int position) {
        return mImagePaths.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {

        final ViewHolder viewHolder;
        if(convertView==null) {
            convertView=mInflater.inflate(R.layout.item_griview, parent,false);

            viewHolder=new ViewHolder();
            viewHolder.mImg=(ImageView) convertView.findViewById(R.id.id_item_image);
            viewHolder.mSelect=(ImageButton) convertView.findViewById(R.id.id_item_select);
            convertView.setTag(viewHolder);//使得下次可以直接getTag;
        }
        else {
            viewHolder=(ViewHolder) convertView.getTag();
        }

        //重置状态,防止第一屏的图片以及表示已经选择状态图标影响第二屏图片的显示
        //因为第二屏的item有可能是第一屏item的复用,而复用的item中的imageView还设置的是第一屏显示的图案
        //这里与imageView去setTag还是有所区别的
        viewHolder.mImg.setImageResource(R.drawable.pictures_no);
        viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);
        viewHolder.mImg.setColorFilter(null);

        viewHolder.mImg.setMaxWidth(mScreenWidth/3);
        //因为已经在布局中设置一行显示三个图片,所以在这里可以设置mImg的MaxWidth(这行代码可选)
        //这样就可以优化ImageLoader中获取ImageView的宽度的那段代码(在某些情况下优化效果是比较明显的)

        ImageLoader.getInstance(3, Type.LIFO).loadImage(
                mDirPath + "/" + mImagePaths.get(position),viewHolder.mImg);

        final String filePath=mDirPath+"/"+mImagePaths.get(position);

        viewHolder.mImg.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {       
                //如果已经被选择
                if(mSelectedImg.contains(filePath)) {
                    mSelectedImg.remove(filePath);
                    viewHolder.mImg.setColorFilter(null);
                    viewHolder.mSelect.setImageResource(R.drawable.picture_unselected);
                }
                else {
                    mSelectedImg.add(filePath);
                    viewHolder.mImg.setColorFilter(Color.parseColor("#77000000"));
                    viewHolder.mSelect.setImageResource(R.drawable.pictures_selected);
                }
//              notifyDataSetChanged();
                //如果调用该方法会使得在点击屏幕的时候会出现闪屏的情况,所以直接在onClick中用set方法设置比较好,就不要调用该方法了
                //notifyDataSetChanged方法通过一个外部的方法控制如果适配器的内容改变时需要强制调用getView来刷新每个Item的内容
                //可以实现动态的刷新列表的功能
            }
        });

        //每次GridView的状态改变时(如滑动GridView),或者选择另外一个文件夹,就需要调用getView来重绘视图
        //如果不加下面这段代码,GridView的状态改变时或者选择另外一个文件夹,原来已经被选择的图片就不会显示被选择的效果
        //但是实际上该图片已经被选择了
        if (mSelectedImg.contains(filePath)) {
            viewHolder.mImg.setColorFilter(Color
                    .parseColor("#77000000"));
            viewHolder.mSelect
                    .setImageResource(R.drawable.pictures_selected);
        }

        return convertView;//这里返回的就是GridView的item为item_griview.xml中布局的关键
    }


    private class ViewHolder {
        ImageView mImg;
        ImageButton mSelect;
    }
}

接着是选择不同文件夹要用到的弹窗(PopupWindow):自定义的ListImageDirPopupWindow

public class ListImageDirPopupWindow extends PopupWindow {
    private int mWidth;
    private int mHeight;
    private View mConVertView;
    private ListView mListView;
    private List<FolderBean> mDatas;

    public OnDirSelectedListener mListener;

    public void setOnDirSelectedListener(OnDirSelectedListener mListener) {
        this.mListener = mListener;
    }

    public interface OnDirSelectedListener {
        void onSeleted(FolderBean folderBean);
    }

    public ListImageDirPopupWindow(Context context, List<FolderBean> mDatas) {
        calWidthAndHeight(context);

        mConVertView = LayoutInflater.from(context).inflate(
                R.layout.popup_main, null);
        this.mDatas = mDatas;

        setContentView(mConVertView);
        setWidth(mWidth);
        setHeight(mHeight);

        setFocusable(true);// 使得窗口可以从当前焦点小部件中抓取焦点
        setTouchable(true);// 可以点击
        setOutsideTouchable(true);// 可以点击范围之外的地方
        setBackgroundDrawable(new BitmapDrawable());

        setTouchInterceptor(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                    dismiss();
                    return true;
                }
                return false;
            }
        });

        initViews(context);
        initEvent();
    }

    private void initEvent() {
        mListView.setOnItemClickListener(new OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id) {
                if(mListener!=null) {
                    mListener.onSeleted(mDatas.get(position));
                }
            }
        });
    }

    private void initViews(Context context) {
        mListView = (ListView) mConVertView.findViewById(R.id.id_list_dir);
        mListView.setAdapter(new ListDirAdapter(context,mDatas));
    }

    /**
     * 计算popupWindow的宽和高
     * 
     * @param context
     */
    private void calWidthAndHeight(Context context) {
        WindowManager wm = (WindowManager) context
                .getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        mWidth = outMetrics.widthPixels;
        mHeight = (int) (outMetrics.heightPixels * 0.7);
    }

    private class ListDirAdapter extends ArrayAdapter<FolderBean> {
        private LayoutInflater mInflater;
        private List<FolderBean> mDatas;

        public ListDirAdapter(Context context,List<FolderBean> objects) {
            super(context, 0, objects);

            mInflater = LayoutInflater.from(context);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;

            if (holder == null) {
                holder = new ViewHolder();
                convertView = mInflater.inflate(R.layout.item_popup_main,
                        parent, false);
                holder.mImg = (ImageView) convertView
                        .findViewById(R.id.id_id_dir_item_image);
                holder.mDirName = (TextView) convertView
                        .findViewById(R.id.id_dir_item_name);
                holder.mDirCount = (TextView) convertView
                        .findViewById(R.id.id_dir_item_count);

                convertView.setTag(holder);
            } else
                holder = (ViewHolder) convertView.getTag();

            FolderBean bean=getItem(position);

            //重置
            holder.mImg.setImageResource(R.drawable.pictures_no);

            ImageLoader.getInstance().loadImage(bean.getFirstImgPath(), holder.mImg);

            holder.mDirCount.setText(""+bean.getCount());
            holder.mDirName.setText(bean.getName());

            return convertView;
        }

        private class ViewHolder {
            ImageView mImg;
            TextView mDirName;
            TextView mDirCount;
        }
    }
}

为了解耦,在ListImageDirPopupWindow的initEvent()方法中设置LIstView的item的onClick事件,然后设置一个接口,用一个接口进行回调,在MainActivity中实现该接口,并且编写需要的逻辑代码

以及PopupWindow中涉及的FolderBean

public class FolderBean {
    /**
     * 当前文件夹的路径
     */
    private String dir;
    /**
     * 当前文件夹下第一张图片的路径
     */
    private String firstImgPath;
    /**
     * 当前文件夹的名称
     */
    private String name;
    /**
     * 当前文件夹中图片的数量
     */
    private int count;

    public FolderBean() {

    }

    public String getDir() {
        return dir;
    }

    public void setDir(String dir) {
        this.dir = dir;

        int lastIndexOf=this.dir.lastIndexOf("/");
        this.name=this.dir.substring(lastIndexOf+1);
    }

    public String getFirstImgPath() {
        return firstImgPath;
    }

    public void setFirstImgPath(String firstImgPath) {
        this.firstImgPath = firstImgPath;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public String getName() {
        return name;
    }
}

最后就是 MainActivity

public class MainActivity extends Activity {    
    private static final int DATA_LOADED=0x110;

    private GridView mGridView;
    private List<String> mImgs;
    private ImageAdapter mImgAdapter;

    private RelativeLayout mBottonLy;
    private TextView mDirName;
    private TextView mDirCount;

    private File mCurrentDir;
    private int mMaxCount;

    private List<FolderBean> mFolderBeans = new ArrayList<FolderBean>();

    private ProgressDialog mProgressDialog;

    //实现LIstView的item的onClick事件相关的接口
    private ListImageDirPopupWindow mPopupWindow;

    private Handler mHandler=new Handler() {
        public void handleMessage(android.os.Message msg) {
            if(msg.what==DATA_LOADED) {     
                //绑定数据到View中
                data2View();

                initPopuWindow();
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initDatas();
        initEvent();
    }

    protected void data2View() {
        if(mCurrentDir==null) {
            Toast.makeText(this, "未扫描到任何图片", Toast.LENGTH_SHORT).show();
            return;
        }

        mImgs=Arrays.asList(mCurrentDir.list(new FilenameFilter() {

            @Override
            public boolean accept(File dir, String filename) {
                if(filename.endsWith(".jpg")||filename.endsWith(".jpeg")||filename.endsWith(".png"))
                    return true;
                return false;
            }
        }));
        mImgAdapter=new ImageAdapter(this, mImgs, mCurrentDir.getAbsolutePath());
        mGridView.setAdapter(mImgAdapter);

        mDirCount.setText(""+mMaxCount);
        mDirName.setText(mCurrentDir.getName());
    }


    private void initEvent() {
        mBottonLy.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                mPopupWindow.setAnimationStyle(R.style.dir_popupwindow_anim);//设置popupWindow显示与收起来的动画(如果不想实现动画可以注释掉,无任何影响,但是要实现动画一定要加上文章末尾的几步)
                mPopupWindow.showAsDropDown(mBottonLy, 0, 0);//设置显示PopupWindow的位置位于指定View的左下方,x,y表示坐标偏移量

                lightOff();
            }
        });
    }

    private void initPopuWindow() {
        mPopupWindow=new ListImageDirPopupWindow(this, mFolderBeans);

        //在popupWindow出现 时候,会有屏幕变暗的特效,所以要在这里设置popupWindow消失的时候的事件,使屏幕变回原来的亮度
        mPopupWindow.setOnDismissListener(new OnDismissListener() {

            @Override
            public void onDismiss() {
                lightOn();
            }
        });

        mPopupWindow.setOnDirSelectedListener(new OnDirSelectedListener() {
            @Override
            public void onSeleted(FolderBean folderBean) {
                mCurrentDir=new File(folderBean.getDir());

                //由于mImgs每次都是根据已有文件夹来搜索图片然后传入GridView的适配器中,
                //所以在已有文件夹下增删图片之后重新在popupWindow中打开该文件夹是能够刷新数据的(也就是文件夹下的所有图片的新的总数)
                //而由于所有有图片的文件夹是在程序一开启的时候扫描完成的,
                //所以之后如果不重启程序而新增文件夹且在文件夹下增加图片是不会被扫描的,除非重启程序
                //或者在未关闭程序的时候删除已经扫描的到的文件夹,然后再程序中从PopupWindow中选择该文件夹(因为此时ListView的数据还没刷新所以还暂时存在于PopupWindow中)会因异常退出
                mImgs=Arrays.asList(mCurrentDir.list(new FilenameFilter() {

                    @Override
                    public boolean accept(File dir, String filename) {
                        if(filename.endsWith(".jpg")||filename.endsWith(".jpeg")||filename.endsWith(".png"))
                            return true;
                        return false;
                    }
                }));                
                mImgAdapter=new ImageAdapter(MainActivity.this, mImgs, folderBean.getDir());
                mGridView.setAdapter(mImgAdapter);

                mDirCount.setText(mImgs.size()+"");
                mDirName.setText(folderBean.getName());

                mPopupWindow.dismiss();

                //如果将程序挂在后台,然后在程序打开已经存在的目录进行增删图片,重进程序GridView显示的内容是不会更新的,
                //如果重新打开该目录所对应的文件,GridView显示内容会更新,但是PopupWindow中的ListView的文件夹下图片的数量不会更新
                //所以为了更新ListView的数据,需要加上下面的代码
                //且因为ListDirAdapter适配器中的getView方法(ListView一旦有变动就会调用getView重绘)有设置ListView的文件夹下图片的数量,
                //所以不需要另外调用ListDirAdapter实例的notifyDataSetChanged方法刷新数据
                for(FolderBean fb:mFolderBeans) {               if(fb.getDir().substring(fb.getDir().lastIndexOf("/")+1).equals(mCurrentDir.getName())) {
                        if(fb.getCount()!=mImgs.size())
                            fb.setCount(mImgs.size());
                    }
                }
            }
        });
    }

    /**
     * 内容区域变暗
     */
    private void lightOff() {
        WindowManager.LayoutParams lp=getWindow().getAttributes();//Attributes-属性
        lp.alpha=0.3f;
        getWindow().setAttributes(lp);
    }

    /**
     * 内容区域变亮
     */
    private void lightOn() {
        WindowManager.LayoutParams lp=getWindow().getAttributes();//Attributes-属性
        lp.alpha=1.0f;
        getWindow().setAttributes(lp);
    }

    /**
     * 利用ContentProvider扫描手机中的所有图片
     */
    private void initDatas() {
        if (!Environment.getExternalStorageState().equals(
                Environment.MEDIA_MOUNTED)) {
            Toast.makeText(this, "当前存储卡不可用!", Toast.LENGTH_SHORT).show();
            return;
        }

        mProgressDialog = ProgressDialog.show(this, null, "Loading...");

        new Thread() {
            public void run() {         
                Uri mImgUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                //Media.EXTERNAL_CONTENT_URI——The content:// style URI for the "primary" external storage volume,primary:原始的,第一位
                //MediaStore这个类是android系统提供的一个多媒体数据库

                ContentResolver cr = MainActivity.this.getContentResolver();//内容提供器

                //这里需要注意"=? or "的空格不能丢
                Cursor cursor=cr.query(mImgUri, null, MediaStore.Images.Media.MIME_TYPE
                        + "=? or " + MediaStore.Images.Media.MIME_TYPE + "=?",
                        new String[] { "image/jpeg", "image/png" },
                        MediaStore.Images.Media.DATE_MODIFIED);//最后一个是排序方式,以图片的日期作为依据

                Set<String> mDirPaths=new HashSet<String>();//用于存储包含图片的文件夹的路径

                while(cursor.moveToNext()) {
                    String path=cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
                    File parentFile=new File(path).getParentFile();
                    if(parentFile==null) continue;//这里需要判断一下,有时候parentFile会出现为null的情况,虽然不知道具体原因,有可能是因为图片被隐藏的原因

                    String dirPath=parentFile.getAbsolutePath();//得到绝对路径

                    if(mDirPaths.contains(dirPath)) continue;//防止重复遍历相同文件夹下的图片
                    else {
                        mDirPaths.add(dirPath);
                        FolderBean folderBean=new FolderBean();
                        folderBean.setDir(dirPath);
                        folderBean.setFirstImgPath(path);

                        if(parentFile.list()==null) continue;
                        //parentFile.list():返回这个文件所代表的目录中的文件名的字符串数组。如果这个文件不是一个目录,结果是空的

                        int picSize=parentFile.list(new FilenameFilter() {
                            //设置过滤,防止非图片被计算
                            @Override
                            public boolean accept(File dir, String filename) {
                                if(filename.endsWith(".jpg")||filename.endsWith(".jpeg")||filename.endsWith(".png"))
                                    return true;
                            return false;
                            }
                        }).length;

                        folderBean.setCount(picSize);

                        mFolderBeans.add(folderBean);

                        if(picSize>mMaxCount) {
                            mMaxCount=picSize;
                            mCurrentDir=parentFile;
                        }
                    }       
                }
                cursor.close();

                //通知Handler扫描图片完成
                mHandler.sendEmptyMessage(DATA_LOADED);
            }
        }.start();
    }

    private void initView() {
        mGridView = (GridView) findViewById(R.id.id_gridview);
        mBottonLy = (RelativeLayout) findViewById(R.id.id_bottom_ly);
        mDirName = (TextView) findViewById(R.id.id_dir_name);
        mDirCount = (TextView) findViewById(R.id.id_dir_count);
    }
}

当然,还有相关的布局文件和要添加的权限:
1.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" >

    <GridView
        android:id="@+id/id_gridview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:cacheColorHint="@android:color/transparent"
        android:gravity="center"
        android:clipChildren="true"
        android:listSelector="@android:color/transparent"
        android:numColumns="3"
        android:stretchMode="columnWidth"
        android:verticalSpacing="3dp"
        android:horizontalSpacing="3dp" >
    </GridView>

    <RelativeLayout
        android:id="@+id/id_bottom_ly"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"
        android:background="#e0000000"
        android:clipChildren="true" >

        <TextView
            android:id="@+id/id_dir_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:paddingLeft="10dp"
            android:text="所有图片"
            android:textColor="@android:color/white" />

        <TextView
            android:id="@+id/id_dir_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:paddingRight="10dp"
            android:text="未知张数"
            android:textColor="@android:color/white" />
    </RelativeLayout>

</RelativeLayout>

android:stretchMode=”columnWidth”
参考网址:http://blog.csdn.net/java2009cgh/article/details/34836967

android:cacheColorHint=”@android:color/transparent” //去除拖动时默认的黑色背景
参考网址:
http://zhidao.baidu.com/link?url=spOGBRiyb150CGoNPMdUVU3cLGqzF-_bjC0GOSEHYLp9AR17ywM0NaQGmWRYUHpTIBOn6baKK46Q-h-xOGRVpKTDkq4_0c38VkZu4zo9aKe
把cacheColorHint这个属性去掉的话,滑动ListView的话会看到item一闪一闪的变颜色,cacheColorHint从字面上就可以看出和缓存有关,一般是设置为null或者是#00000000(透明)也可以

android:listSelector=”@android:color/transparent” //防止在拖拽的时候闪现出黑色
参考网址:http://blog.csdn.net/gchk125/article/details/7586401

android:clipChildren的意思:是否限制子View在其范围内
参考网址:http://www.cnblogs.com/over140/p/3508335.html

2.item_griview.xml (GridView的item布局)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ImageView
        android:id="@+id/id_item_image"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        android:src="@drawable/pictures_no" />
    <!--
         android:scaleType是控制图片如何resized/moved来匹对ImageView的size 
         centerCrop  按比例扩大图片的size居中显示,使得图片长(宽)等于或大于View的长(宽)
    -->

    <ImageButton
        android:id="@+id/id_item_select"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginRight="3dp"
        android:layout_marginTop="3dp"
        android:background="@null"
        android:clickable="false"
        android:src="@drawable/picture_unselected" />
    <!-- android:clickable="false" 默认不被选择 -->

</RelativeLayout>

3.popup_main.xml (弹窗的布局)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff" >

    <ListView
        android:id="@+id/id_list_dir"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="#eee3d9"
        android:dividerHeight="1px" >
    </ListView>

</RelativeLayout>

4.item_popup_main.xml (PopupWindow 中ListView的item的布局)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="5dp" >

    <ImageView
        android:id="@+id/id_id_dir_item_image"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:background="@drawable/pic_dir"
        android:paddingBottom="17dp"
        android:paddingLeft="12dp"
        android:paddingRight="12dp"
        android:paddingTop="9dp"
        android:scaleType="fitXY"
        android:src="@drawable/ic_launcher" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="10dp"
        android:layout_toRightOf="@+id/id_id_dir_item_image"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/id_dir_item_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="所有图片"
            android:textSize="12sp" />

        <TextView
            android:id="@+id/id_dir_item_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="暂无"
            android:textSize="10sp"
            android:textColor="#444"/>
    </LinearLayout>

    <ImageView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:layout_marginRight="20dp"
        android:src="@drawable/dir_choose"/>

</RelativeLayout>

要添加的权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


哦,对了,如果要实现PopupWindow的弹出与收起的动画话,所以还需要小小的几步:
1.在res下新建文件夹,并且添加两个xml文件
slide_down.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate 
        android:fromXDelta="0"
        android:toXDelta="0"
        android:fromYDelta="0"
        android:toYDelta="100%"
        android:duration="200"/>
</set>

slide_up.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate 
        android:fromXDelta="0"
        android:toXDelta="0"
        android:fromYDelta="100%"
        android:toYDelta="0"
        android:duration="200"/>
</set>

2.在res/values下的styles.xml中添加下面几行代码:

<style name="dir_popupwindow_anim">
        <item name="android:windowEnterAnimation">@anim/slide_up</item>
        <item name="android:windowExitAnimation">@anim/slide_down</item>
    </style>

源码http://download.csdn.net/download/qq_22804827/9420428


当然,这个程序还存在着可以改善的地方,比如:

1.如果将程序挂在后台,然后新建一个文件夹,在里面添加一些图片,然后重进程序,该文件夹不能被扫描到,除非重启程序

2.在已有文件夹下增减图片,如果不在PopupWindow中重新点击该文件夹,增减的图片是不会刷新的
//必须在查找前进行全盘的扫描,否则新加入的图片是无法得到显示的(加入对sd卡操作的权限) 参考网址:http://www.2cto.com/kf/201305/214899.html

//sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,
//Uri.parse("file://"+ Environment.getExternalStorageDirectory())));

如果有想法的,可以去完善完善哦!


新知识点总结:
ImageLoader的getImageViewFieldValue方法中Field的使用

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值