Android开发 之 图片浏览

1.简介

图片浏览在app中是很常见的,本文使用android的recyclerView ,viewPager, Bitmap,等一系列组件开发的一个浏览器;

实现大图无损加载,大图压缩成小图浏览;

2.开发要点:

使用recycleview显示小图浏览,使用自定义的GridlayoutManager,设置了滑动速度;

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int a = super.scrollVerticallyBy((int)(speedRatio*dy), recycler, state);//屏蔽之后无滑动效果,证明滑动的效果就是由这个函数实现
        if(a == (int)(speedRatio*dy)){
            //Log.e("speed",dy+"---"+a);
            return dy;
        }
        return a;
    }

    /**
     * 设置滑动速度因子;
     * @param speedRatio
     */
    public void setSpeedRatio(float speedRatio){
        this.speedRatio = speedRatio;
    }

上面的dy就是滑动速度实时变化值,通过滑动因子改变它;监听recycleView的onTouchListener,返回false不拦截事件,继续想上层传递;

这里做到了不抬起手的时候滑动速度不变,当抬起手指的时候滑动因为为原来速度的0.3倍;

为什么要设置滑动速度?因为速度太快的话,突然滑到底部的时候,图片加载很慢的话会卡顿;

        recycleview.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if(motionEvent.getAction()==MotionEvent.ACTION_UP)
                {
                    myGridLayoutManager.setSpeedRatio(0.3f);
                }else{
                    myGridLayoutManager.setSpeedRatio(1f);
                }
                return false;
            }
        });



数据:使用相册的mediastota数据库中保存的图片路径; 通过内容解析者去获取,存到一个list中,刷新adapter显示数据;

        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);

        while (cursor.moveToNext())
        {
            int index = cursor
                    .getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            String path = cursor.getString(index); // 文件地址
            //Log.e("address",path);
            list.add(path);
        }
        pictureAdapter.notifyDataSetChanged();


至于适配器中的优化是recycleView的适配器这里不多讲;直接看代码:

    public class PictureAdapter extends RecyclerView.Adapter
    {
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            ImageView inflate = (ImageView) LayoutInflater.from(MainActivity.this).inflate(R.layout.item_picture, parent,false);
            inflate.setLayoutParams(new RecyclerView.LayoutParams(bianchang,bianchang));
            return new MyViewHolder(inflate);
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            MyViewHolder myViewHolder= (MyViewHolder) holder;
            myViewHolder.setData(position);
        }

        @Override
        public int getItemCount() {
            return list.size();
        }
    }
    public class MyViewHolder extends RecyclerView.ViewHolder
    {

        private ImageView imageView;
        private String picturePath;
        private int position;

        public MyViewHolder(View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.iv);
            imageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    float x = imageView.getX();
                    float y = imageView.getY();
                    Bundle bundle=new Bundle();
                    bundle.putFloat("x",x);
                    bundle.putFloat("y",y);
                    scroolPosition = position;
                    bundle.putInt("position",position);
                    bundle.putString("picturepath",picturePath);
                    Intent intent = new Intent(MainActivity.this, PictureInfoActivity.class);
                    intent.putExtra("positon",bundle);
                    startActivity(intent);
                    overridePendingTransition(0,0);
                }
            });
        }

        public void setData(final int data) {
            imageView.setImageBitmap(null);
            position = data;
            picturePath = list.get(data);
            LoadPictureUtils.loadPicture(bianchang,picturePath,imageView);
        }
    }

上面的代码中,加载图片使用线程池异步加载图片,而且进行缩放成360*360的尺寸;所以这里要进行大量的计算必须放在子线程中;线程中执行的是Runable类对象的任务;所以我们要继承runable类,重写run方法:下面有一个复用bitmap技术,从一个set集合中取出可以复用的bitmap对象,这个set是当bitmap缓存LruCache中移除多余的bitmap放到set中的,使用的是弱引用的bitmap;

当图片加载缩放的bitmap存放到内存缓存中LurCaChe中;LurCaChe是一个内存缓存结合,设置缓存大小,超过缓存则移除之前的,存入新bitmap;

    @Override
    public void run() {
        if (biancheng == 1) {
            loadBigpicture();
        } else {
            loadscalepicture();
        }
    }

    /**
     * 加载缩略图
     */
    public void loadscalepicture() {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        Bitmap bitmap = BitmapFactory.decodeFile(path, options);
        int outWidth = options.outWidth;
        int outHeight = options.outHeight;
        int size = 1;
        if (outWidth > outHeight) {
            size = outWidth / biancheng;
        } else {
            size = outHeight / biancheng;
        }
        options.inSampleSize = size;
        options.inJustDecodeBounds = false;
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        options.inMutable = true;//设置成异变得才能复用内存;
        Bitmap reusable = BitmapCache.getInstence().getReusable(options);
        if (reusable != null) {
            Log.e("hah", "复用bitmap了");
        }
        options.inBitmap = reusable;
        bitmap = BitmapFactory.decodeFile(path, options);
        //Log.e("tag","--"+size+"--"+bitmap.getWidth()+"--"+bitmap.getHeight());

        Message message = new Message();
        message.what = 0;
        Bundle bundle = new Bundle();
        MyData myData = new MyData();
        myData.setBitmap(bitmap);
        myData.setmImageView(mImageView);
        myData.setPath(path);

        bundle.putSerializable("bitmap", myData);
        message.setData(bundle);
        mHandler.sendMessage(message);
        BitmapCache.getInstence().putBitmapToCache(path, bitmap);
    }


获取可以复用的bitmap对象:

 /**
     * 可被复用的Bitmap必须设置inMutable为true;
     Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),
     inSampleSize为1的Bitmap才可以复用;
     Android4.4(API 19)之前被复用的Bitmap的inPreferredConfig
     会覆盖待分配内存的Bitmap设置的inPreferredConfig;
     Android4.4(API 19)之后被复用的Bitmap的内存
     必须大于等于需要申请内存的Bitmap的内存;
     */
    public synchronized Bitmap getReusable(BitmapFactory.Options options){
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB){
            return null;
        }
        Bitmap reusable = null;
        Iterator> iterator = setpool.iterator();
        //迭代查找符合复用条件的Bitmap
        try{
            while (iterator.hasNext()){
                Bitmap bitmap = iterator.next().get();
                if (null != bitmap){
                    //检查是否可以被复用
                    if (checkInBitmap(bitmap,options)){
                        reusable = bitmap;
                        //移出复用池
                        iterator.remove();
                        break;
                    }
                }else{
                    iterator.remove();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return reusable;
    }

    static boolean checkInBitmap(Bitmap bitmap,BitmapFactory.Options targetOptions){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // From Android 4.4 (KitKat) onward we can re-use if the byte size of
            // the new bitmap is smaller than the reusable bitmap candidate
            // allocation byte count.
            int width = targetOptions.outWidth / targetOptions.inSampleSize;
            int height = targetOptions.outHeight / targetOptions.inSampleSize;
            int byteCount = width * height * getBytesPerPixel(bitmap.getConfig());
            Log.e("haha",byteCount+"----"+bitmap.getAllocationByteCount());
            return byteCount <= bitmap.getAllocationByteCount();
        }

        // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
        return bitmap.getWidth() == targetOptions.outWidth
                && bitmap.getHeight() == targetOptions.outHeight
                && targetOptions.inSampleSize == 1;
    }

    static int getBytesPerPixel(Bitmap.Config config){
        if (config == Bitmap.Config.ARGB_8888) {
            return 4;
        } else if (config == Bitmap.Config.RGB_565) {
            return 2;
        } else if (config == Bitmap.Config.ARGB_4444) {
            return 2;
        } else if (config == Bitmap.Config.ALPHA_8) {
            return 1;
        }else{
            return 2;
        }
    }


lrucache对象的实例化:

    public void init(Context context)
    {
        //创建一个set集合存放bitmap的弱引用;
        setpool =Collections.synchronizedSet(new HashSet>());
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        int memoryClass = am.getMemoryClass();
        Log.e("HHHH",memoryClass+"M");
        lruCache = new LruCache((memoryClass/5)*1024*1024)
        {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT){
                    return value.getAllocationByteCount();
                }
                return value.getByteCount();
            }

            /**
             * 移除bitmap的时候回调这个函数;
             * @param evicted
             * @param key
             * @param oldValue
             * @param newValue
             */
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                //super.entryRemoved(evicted, key, oldValue, newValue);
                if(oldValue.isMutable())
                {
                    //3.0一下,bitmap的内存实在native中申请的;
                    //3.0以上是在java层申请的;
                    //8.0又到了native层;
                    setpool.add(new WeakReference(oldValue,initReferenceQueue()));
                }else{
                    oldValue.recycle();
                }
            }
        };

    }

    public void addSetPool(Bitmap bitmap)
    {
        setpool.add(new WeakReference(bitmap,initReferenceQueue()));
    }

    public void putBitmapToCache(String key,Bitmap bitmap)
    {
        lruCache.put(key,bitmap);
    }
    public Bitmap getBitmapFromCache(String key)
    {
         return  lruCache.get(key);
    }
    public void clearCache()
    {
        lruCache.evictAll();
    }



在RecycleView的adapter中的Viewholder中item的点击事件,获取点击view的xy值和当前位置position,已经当前view的图片路径传给下一个显示大图的activity中;这里禁止了系统activity的跳转动画效果,因为我们自己要定义共享元素动画效果,可以兼容到android5.0以下;

            imageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    float x = imageView.getX();
                    float y = imageView.getY();
                    Bundle bundle=new Bundle();
                    bundle.putFloat("x",x);
                    bundle.putFloat("y",y);
                    scroolPosition = position;
                    bundle.putInt("position",position);
                    bundle.putString("picturepath",picturePath);
                    Intent intent = new Intent(MainActivity.this, PictureInfoActivity.class);
                    intent.putExtra("positon",bundle);
                    startActivity(intent);
                    overridePendingTransition(0,0);
                }
            });


跳转过去后的activity中是一个ViewPager,再次要给activity设置透明的,通过主题设置;然后获取viewpager中的当前显示的view做输定动画效果;

这里有一个难点,如何在activity打开的时候获取ViewPager当前要显示的view对象呢?大概过程是这样现场见viewpager的item,然后在绘制view;所以在绘制之前要给view设置属性动画;view绘制的时候可以给view设置视图树监听,但是Item的创建的时候怎么样去拿到这个view呢;看下面的代码就知道了,自定义adapter然后写一个监听方法,设置监听,item view在创建的时候会调用setPrimaryItem()这个方法,在这个方法中传递view到这个接口中;

 /**
         * 这个方法当页面改变的时候会调用四次,第一次的position是上个页面的,后面三个的position是当前页面的;
         * 当第一进入的viewpager的时候四次的position都是当前显示的位置;
         * @param container
         * @param position
         * @param object
         */
        @Override
        public void setPrimaryItem(ViewGroup container, int position, Object object) {
            super.setPrimaryItem(container, position, object);
            if(position!=currentPosition)
            {
                //滑到下一页面的时候;
                currentPosition=position;
                mCurrentView= (View) object;
                mCurrentpath = list.get(position);
                Log.e("setPrimaryItem",position+"---"+mCurrentpath+"----"+mCurrentView);
                if(!isFirst)
                {
                    Log.e("hhah","动画没有结束调用");
                    LoadPictureUtils.loadBigPicture(mCurrentpath, (ImageView) mCurrentView);
                }
                MMsg mMsg=new MMsg();
                mMsg.setWhat(3);
                mMsg.setObject(currentPosition);
                MsgEventManager.getInstance().sendMsg(mMsg,"MainActivity");
            }
            if(listener!=null)
            {
                listener.currentItem((ImageView) mCurrentView);
            }
            Log.e("setPrimaryItem",""+position);
        }

再通过view设置视图树监听view的绘制,这样共享动画就做好了;

        vpAdapter.setGetCurrentListener(new GetCurrentItme() {
            @Override
            public void currentItem(ImageView imageView) {
                vpAdapter.removeGetCurrentListener();
                iv=imageView;
                Log.e("xy", iv.getX()+"---"+ iv.getY());
                iv.setTop((int) y);//margin和xy不要混用;
                iv.setLeft((int) x);

                iv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        Log.e("xy", iv.getX()+"---"+ iv.getY()+"---"+iv.getWidth());
                        int width = rl_content.getWidth();
                        int height = rl_content.getHeight();
                        ObjectAnimator oa = ObjectAnimator.ofInt(iv, "top", (int)y, 0);
                        ObjectAnimator oa2 = ObjectAnimator.ofInt(iv, "left", (int)x, 0);
                        ObjectAnimator oa3 = ObjectAnimator.ofInt(iv, "bottom", (int)(y+360), height);
                        ObjectAnimator oa4 = ObjectAnimator.ofInt(iv, "right", (int)(x+360), width);
                        ValueAnimator colorAnim = ObjectAnimator.ofInt(rl_content, "backgroundColor", start, end);
                        colorAnim.setEvaluator(new ArgbEvaluator());
                        ValueAnimator valueAnimator = ValueAnimator.ofInt(1);
                        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                            @Override
                            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                                iv.setImageBitmap(bitmapFromCache);
                            }
                        });
                        AnimatorSet as=new AnimatorSet();
                        as.playTogether(oa,oa2,oa3,oa4,valueAnimator,colorAnim);//一起飞。
                        as.setDuration(300);
                        as.start();
                        as.addListener(new Animator.AnimatorListener() {
                            @Override
                            public void onAnimationStart(Animator animator) {

                            }

                            @Override
                            public void onAnimationEnd(Animator animator) {
                                vpAdapter.loadFirstBigPicture();
                            }

                            @Override
                            public void onAnimationCancel(Animator animator) {

                            }

                            @Override
                            public void onAnimationRepeat(Animator animator) {

                            }
                        });
                        if (Build.VERSION.SDK_INT >= 16) {
                            iv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                        } else {
                            iv.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                        }
                    }
                });

            }
        });


在SetPrimaryItem()中没切换都会调用加载大图的方法,加载大图先要判断当前内存是否够用,够用的话直接加载原图进来,但是在绘制原图的时候如果图片的尺寸过大的会绘制不了的;需要view关系硬件加速;下面的方法也是在runable的run方法中执行的;加载后把bitmap引用保存在本类中,设置为静态的,等下次加载原图的时候复用这个bitmap对象;

   /**
     * 加载原图
     */
    public void loadBigpicture() {
        int[] maxTextureSize = new int[1];//canvs中一次绘制的最大尺寸;
        GLES10.glGetIntegerv(GL10.GL_MAX_TEXTURE_SIZE, maxTextureSize, 0);
        float maxSize = 4048;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        Bitmap bitmap = BitmapFactory.decodeFile(path, options);
        int outWidth = options.outWidth;
        int outHeight = options.outHeight;
        if (outWidth * outHeight * 4 < (am.getMemoryClass() * 1024 * 1024)) {
            Log.e("getMemoryClass", am.getMemoryClass() + "M" + (outWidth * outHeight * 4 / 1024 / 1024));
        }
        //Log.e("tag","--"+size+"--"+bitmap.getWidth()+"--"+bitmap.getHeight());
        options.inSampleSize = 1;
        if(cachebitmap!=null)
        {
            boolean b = BitmapCache.checkInBitmap(cachebitmap, options);
            if (b) {
                Log.e("hah", "大图复用bitmap了");
                options.inBitmap = cachebitmap;
            }else if(!cachebitmap.isRecycled()){
                cachebitmap.recycle();
            }
        }
        options.inMutable = true;//设置成异变得才能复用内存;
        options.inJustDecodeBounds = false;
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        bitmap = BitmapFactory.decodeFile(path, options);
        Message message = new Message();
        message.what = 1;
        Bundle bundle = new Bundle();
        MyData myData = new MyData();
        myData.setUitils(uitils);
        myData.setBitmap(bitmap);
        myData.setmImageView(mImageView);
        myData.setPath(path);

        bundle.putSerializable("bitmap", myData);
        message.setData(bundle);
        mHandler.sendMessage(message);
        cachebitmap = bitmap;
    }


当ViewPager进行切换的时候,小图浏览的activity中也需要更新的,这里使用了发消息的机制,去通知小图更新,这个消息机制是类似于系统的广播;小图的activiy中通过recycleView的方法设置滚动的位置;这里设置了+-5的处理方法,保证显示的位置是在中间;

    /**
     * 消息接受者
     */
    MsgEventManager.MsgEvent event=new MsgEventManager.MsgEvent() {
        @Override
        public void msg(Object msg) {
            MMsg mMsg= (MMsg) msg;
            if(mMsg.what==1)
            {
                MsgEventManager.getInstance().sendMsg(list,"PictureInfoActivity");
            }else if(mMsg.what==2)
            {
                int position = (int) mMsg.getObject();
                View itemView = myGridLayoutManager.findViewByPosition(position);
                ImageView imageView = itemView.findViewById(R.id.iv);
                float x = imageView.getX();
                float y = imageView.getY();
                Bundle bundle=new Bundle();
                bundle.putFloat("x",x);
                bundle.putFloat("y",y);
                mMsg.setBundle(bundle);
            }else if(mMsg.what==3)
            {
                //动态自动的滑动recycleView到指定位置;
                int position = (int) mMsg.getObject();
                if(position>scroolPosition)
                {
                    scroolPosition = position;
                    position+=5;
                }else{
                    scroolPosition = position;
                    position-=5;
                }
                recycleview.scrollToPosition(position);
            }
        }
    };


然后在监听返回按键,给当前的view设置属性动画,动画结束的时候finish掉activity,当然在finish的时候方法当前显示的大图bitmap的recyc方法;

但是这里有个问题,设置动画后没有效果,其实需要把这个item单独从viewPager中移除,然后添加在一个布局中,当然也可以添加到系统android.R.id.content布局中,在做动画效果就可以了;

   /**
     * 关闭动画效果;
     */
    public void closeAnimation()
    {
        final Bitmap bitmapFromCache = BitmapCache.getInstence().getBitmapFromCache(mCurrentpath);
        final MyImageView imageView= (MyImageView) mCurrentView;
        imageView.setLayerType(View.LAYER_TYPE_HARDWARE,null);
        ViewGroup parent = (ViewGroup) imageView.getParent();
        parent.removeView(imageView);
        ViewGroup viewGroup = (ViewGroup) findViewById(android.R.id.content);
        viewGroup.addView(imageView);
        MMsg mMsg=new MMsg();
        mMsg.what=2;
        mMsg.setObject(currentPosition);
        MsgEventManager.getInstance().sendMsg(mMsg,"MainActivity");
        Bundle bundle = mMsg.getBundle();
        int x = (int) bundle.getFloat("x");
        int y = (int) bundle.getFloat("y");
        int width = rl_content.getWidth();
        int height = rl_content.getHeight();

        ObjectAnimator oa = ObjectAnimator.ofInt(imageView, "left", 0, x);
        ObjectAnimator oa2 = ObjectAnimator.ofInt(imageView, "top", 0, y);
        ObjectAnimator oa3 = ObjectAnimator.ofInt(imageView, "right",width, (x+360));
        ObjectAnimator oa4 = ObjectAnimator.ofInt(imageView, "bottom", height,(y+360));

        ValueAnimator colorAnim = ObjectAnimator.ofInt(rl_content, "backgroundColor", end, start);
        colorAnim.setEvaluator(new ArgbEvaluator());
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                imageView.setImageBitmap(bitmapFromCache);
            }
        });
        AnimatorSet as=new AnimatorSet();
        as.playTogether(oa,oa2,oa3,oa4,valueAnimator,colorAnim);//一起飞。
        as.setDuration(300);
        as.start();
        as.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                Log.e("zxq",imageView.getTop()+"---"+imageView.getLeft()+"--"+imageView.getRight()+"--"+imageView.getBottom());
                PictureInfoActivity.this.finish();
                overridePendingTransition(0,0);
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
    }


做到这里图片浏览器基本上已经做完了,代码编写的时间较短,可以会存在很多不合理的地方,有待优化;

使用的技术点大概也就上面讲述的,关键是在于细节的处理;



你可能感兴趣的:(Android,案例)