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必须设置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使用Fragment嵌套Fragment的方式实现界面滑动
- Android中adapter的原理简单说明
- android 显示进度的按钮
- ImageView显示图片时,上下出现多余空白。
- 28. android——miniTwitter登录界面 详解
- Android文本输入框EditText方法说明和属性
- android:bug Fragment not attached to Activity ,fragment not a
- Tabhost+picture
- Dialog详解