图片浏览在app中是很常见的,本文使用android的recyclerView ,viewPager, Bitmap,等一系列组件开发的一个浏览器;
实现大图无损加载,大图压缩成小图浏览;
使用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);
}
}
当图片加载缩放的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当前要显示的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);
}
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) {
}
});
}
做到这里图片浏览器基本上已经做完了,代码编写的时间较短,可以会存在很多不合理的地方,有待优化;
使用的技术点大概也就上面讲述的,关键是在于细节的处理;