备注:本文关键内容是“OOM(Out Of Memory)异常",跟 “移动时的截图起点规则”内容,其他部分没看也可以看的懂
写此程序背景
看到微信的图片浏览的强大功能,于是自己写了一个。原则上可以无限放大,但是放大部分 像素必须有原图片的1个像素,最小也不能小于1*1像素。
思路:
功能解剖:
缩放:微信的缩放能缩放到用户满意的范围。(放大不会超过max倍,缩小不会超过min)
移动:当图片高与宽小于屏幕时,能移动图片到任意位置。当高或宽大于屏幕时,移动图片则会截取图片某一模块放大满屏显示。
最重要的一点就是图片放大时看不出来图片变模糊
解剖雏形:
假设用系统自带Matrix函数来控制放大缩小。
缩小: 可以缩小很小倍,当不易控制倍数(如1.25倍,但Matrix不会那么精确)
放大:但放大超出屏幕时,Bitmap.createBitmap会在内存中创建一个很大的图(或内存超出系统设定的值或宽高超出屏幕),导致显存或内存不足。
因为上条放大会出现问题所以本方案绝对不行。
解剖过度:
那么要有那么一种缩放方法满足下面条件
一、能几乎精确的缩小到某一个倍数
二、放大时内存不会溢出
基于缩放的截取方法想出以下移动方案
一、当放大时移动时计算某个参考点在图上移动的位置所占比列(x,y),高宽为屏幕高度/倍数
二、当缩小的图在屏幕范围之内,那么移动的效果通过移动ImageView的位置实现
方案:因为缩放的关键是放大,所以可以考虑放大时用截取一段图*n倍不会溢出的图
截图方案: 一、看到截取就想到用画布canvas解决(于是创建了一个MyBitMap类)能截永远不会内存溢出的放大图。并且图像不会模糊(canvas优秀之处)。
补充:OOM(Out Of Memory)异常
1.放大时不模糊的实现:利用canvas获取放大后的图,就能解决安常规放大后模糊的现状。
2.图片不溢出的实现:在canvas放大时限制图片的大小不超出屏幕就行。
溢出的情况
一、图片的高或宽超出了屏幕。所以在canvas放大时限制图片的大小不超出屏幕就行。
二、某一个bitmap超出了系统对单张图片的限制大小5MB(5MB根据系统不同会有差别)。利用canve截图到的图才几十kb。如果利用常规的Matrix在原来的图基础上来放大(放大后的图片大小为原图大小*当前倍数的平方),就会有超限(5MB)溢出。
import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.widget.ImageView.ScaleType; /** *此处函数是得到剪切的图片 * @author ZhangJianLin * */ public class MyBitMap { public MyBitMap() { // TODO Auto-generated constructor stub } /** * * @param unscaledBitmap the bitmap of source * @param dstWidth what width you want to set * @param dstHeight What width you want to set * @param scalingLogic it is ScaleType * @return the scaled bitmap */ public static Bitmap createScaledBitmap(Bitmap unscaledBitmap, int dstWidth, int dstHeight, ScaleType scalingLogic) { Rect srcRect = calculateSrcRect(unscaledBitmap.getWidth(), unscaledBitmap.getHeight(), dstWidth, dstHeight, scalingLogic); Rect dstRect = calculateDstRect(unscaledBitmap.getWidth(), unscaledBitmap.getHeight(), dstWidth, dstHeight, scalingLogic); Bitmap scaledBitmap = Bitmap.createBitmap(dstRect.width(), dstRect.height(), Config.ARGB_8888); Canvas canvas = new Canvas(scaledBitmap); canvas.drawBitmap(unscaledBitmap, srcRect, dstRect, new Paint(Paint.FILTER_BITMAP_FLAG)); return scaledBitmap; } //根据dstWOrH计算原图应该截取的截图合适的高宽比例图 public static Rect calculateSrcRect(int srcWidth, int srcHeight, int dstWidth, int dstHeight, ScaleType scalingLogic) { if (scalingLogic == ScaleType.CENTER_CROP) { final float srcAspect = (float)srcWidth / (float)srcHeight; final float dstAspect = (float)dstWidth / (float)dstHeight; if (srcAspect > dstAspect) { final int srcRectWidth = (int)(srcHeight * dstAspect); final int srcRectLeft = (srcWidth - srcRectWidth) / 2; return new Rect(srcRectLeft, 0, srcRectLeft + srcRectWidth, srcHeight); } else { final int srcRectHeight = (int)(srcWidth / dstAspect); final int scrRectTop = (int)(srcHeight - srcRectHeight) / 2; return new Rect(0, scrRectTop, srcWidth, scrRectTop + srcRectHeight); } } else { return new Rect(0, 0, srcWidth, srcHeight); } } //根据dstWOrH计算原图应该截取的期望图合适的高宽比例图 public static Rect calculateDstRect(int srcWidth, int srcHeight, int dstWidth, int dstHeight, ScaleType scalingLogic) { if (scalingLogic == ScaleType.FIT_XY) { final float srcAspect = (float)srcWidth / (float)srcHeight; final float dstAspect = (float)dstWidth / (float)dstHeight; if (srcAspect > dstAspect) { return new Rect(0, 0, dstWidth, (int)(dstWidth / srcAspect)); } else { return new Rect(0, 0, (int)(dstHeight * srcAspect), dstHeight); } } else { return new Rect(0, 0, dstWidth, dstHeight); } } /** * * @param unscaledBitmap the bitmap of source * @param scale the scale you want * @param scalingLogic it is ScaleType * @return the scaled bitmap */ //根据放大倍数获得截取图安scale放大的图 public static Bitmap createBMScaleBitmap(Bitmap unscaledBitmap, Double scale, ScaleType scalingLogic){ int dstWidth = (int)(unscaledBitmap.getWidth()* scale); int dstHeight = (int)(unscaledBitmap.getHeight()*scale); return createScaledBitmap(unscaledBitmap, dstWidth, dstHeight, scalingLogic); } }
移动时的截图起点规则
每缩放一次,就以图的中心为截获图的中心(Ox,Oy),起点为(Ox-needwidth/2,Oy - needhight/2)。
在缩放时,把放大后的图在逻辑上的坐标划分为m份,(1单位为屏幕的宽或高),同时每移动一次,就移动1/4份(即每次移动1/4屏幕)。 (单次移动的宽或高像素为 (bitmapWidth/m*n或bitmapHight/m*n,其中n是缩放的倍数,m为计算缩放后的图片高宽跟屏幕对应的宽高的比例,以此得到的值作为x或y的坐标最大值,坐标单位为一个屏幕,每滑次移动半个屏幕或1/3屏幕
结合安卓的滑动或移动的灵敏度,能完美的模拟出效果图。(亲测,如果移动规则用跟踪手的移动位移来移动图片是行不通的,不知道市面上能作出这样的效果是什么样的一个算法,还有待探究)
核心实现代码及注释如下
package com.imageopen; import android.app.Activity; import android.graphics.Bitmap; import android.os.Bundle; import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.view.Window; import android.widget.Button; import android.widget.ImageView; import android.widget.ImageView.ScaleType; public class BigzoonImage extends Activity { ImageView myImageView; Button bigButton;//放大按钮 Button smallButton;//缩小按钮 View myButtons; private Bitmap myBitmap; private double bigSize = 1.25;//每次放大的比列 private double smallSize = 0.8;//每次缩小的比例 double size = 1;//当前放大的倍数 double pixel = 30.00;//限制图片缩小时的最小像素 int bmpWidth;//图片宽度 int bmpHight;//图片高度 int bmpSizeWidth;//放大后的图片宽度bmpwidth*size int bmpSizeHight; int x ;// int y ;// int screenWidth; // 屏幕宽(像素,如:480px) int screenHeight; // 屏幕高(像素,如:800px) int dstHeight; int dstWidth; @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); setContentView(R.layout.bigzoonimage_main); init(); } private void init(){//初始化各参数的值 myImageView = (ImageView)findViewById(R.id.bitmap_image); bigButton = (Button)findViewById(R.id.button_big); smallButton = (Button)findViewById(R.id.button_small); myButtons = (View)findViewById(R.id.bitmap_button); MyBitmapFactory myBitmapFactory = new MyBitmapFactory(this); myBitmap = myBitmapFactory.getDrawBmp(R.drawable.bitmap_test);//事先加载一张图片 myImageView.setImageBitmap(myBitmap); myImageView.setOnTouchListener(ImageOpenListener); bigButton.setOnClickListener(sizeButton);//添加按钮触发事件 smallButton.setOnClickListener(sizeButton);//同上 DisplayMetrics dm = new DisplayMetrics(); //声明一个屏幕像素的类屏幕像素 dm = getResources().getDisplayMetrics(); //得到屏幕像素 screenWidth = dm.widthPixels; // 屏幕宽(像素,如:480px) screenHeight = dm.heightPixels; // 屏幕高(像素,如:800px) } private void big(){//放大时图片的变化 size = bigSize * size; Bitmap newBitmap = myBitmap; newBitmap = bigCal(myBitmap); newBitmap = MyBitMap.createScaledBitmap(newBitmap, dstWidth, dstHeight, ScaleType.FIT_XY); myImageView.setImageBitmap(newBitmap); } private void small(){//缩小时图片的变化 size = smallSize * size; Bitmap newBitmap = myBitmap;//得到原图的截取图 newBitmap = bigCal(newBitmap); newBitmap = MyBitMap.createScaledBitmap(newBitmap, dstWidth, dstHeight, ScaleType.FIT_XY); myImageView.setImageBitmap(newBitmap); } private OnClickListener sizeButton = new OnClickListener() {//放大缩小的事件 @Override public void onClick(View v) { // TODO Auto-generated method stub if(v == bigButton){ big(); } else small(); } }; //每缩放一次,就以图的中心为截获图的中心(Ox,Oy),起点为(Ox-needwidth/2,Oy - needhight/2),宽高为屏幕宽高的图。 public Bitmap bigCal(Bitmap bitmap){//缩放得到原图的截取图 bmpWidth = bitmap.getWidth(); bmpHight = bitmap.getHeight(); //放大的size最小值,16是指限制的截取的图片最小像素,要是缩小的很小,那么一丁点没有意义,这里的16看个人意思 int sizeMax = Math.min(myBitmap.getWidth()/16, myBitmap.getHeight()/16);//限定放大的最大倍数 double sizeMin = Math.max(pixel/myBitmap.getWidth(), pixel/myBitmap.getHeight());//限制缩小的size最小倍数 if(size > sizeMax){ size = sizeMax; } if(size < sizeMin){ size = sizeMin; } bmpSizeWidth = (int)(bmpWidth*size); bmpSizeHight = (int)(bmpHight*size); if(screenWidth > bmpSizeWidth){ x = 0; dstWidth = bmpSizeWidth; }else{ x = (int)((bmpSizeWidth - screenWidth)/(2*size)); bmpWidth = (int)(screenWidth / size); dstWidth = screenWidth; } if(screenHeight > bmpSizeHight){ y = 0; dstHeight = bmpSizeHight; }else{ y = (int)((bmpSizeHight - screenHeight)/(2*size));//放大时计算以中心的为截取中心的所要截图的左上点坐标(x,y) bmpHight = (int)(screenHeight / size); dstHeight = screenHeight; } bitmap = Bitmap.createBitmap(bitmap, x, y, bmpWidth, bmpHight); return bitmap; } //计算缩放的倍数跟屏幕的比例,以得到的值作为x或y的坐标最大值,坐标单位为一个屏幕,每滑动次移动半个屏幕或1/3屏幕 public double rowOrCowNum(int sizeBitmapWH, int screenWH){ double num = (sizeBitmapWH * size)/screenWH; return num; } public Bitmap movCal(Bitmap bitmap, int dx, int dy){//计算移动后的要截取的图 double coordinateX = rowOrCowNum(myBitmap.getWidth() , screenWidth);//缩放后x坐标, double coordinateY = rowOrCowNum(myBitmap.getHeight(), screenHeight);//缩放后y的坐标 if(coordinateX > 1){ if(dx > 0){ x -= (myBitmap.getWidth()/(coordinateX * 4)); if(x < 0){ x = 0; } } if(dx < 0){ x += (myBitmap.getWidth()/(coordinateX *4)); if(x > (myBitmap.getWidth() - bmpWidth)){ x = myBitmap.getWidth() - bmpWidth; } } } if(coordinateY > 1){ if(dy > 0){ y -= (myBitmap.getHeight()/(coordinateY * 4)); if(y < 0){ y = 0; } } if(dy < 0){ y += (myBitmap.getHeight()/(coordinateY *4)); if(y > (myBitmap.getHeight() - bmpHight)){ y = myBitmap.getHeight() - bmpHight; } } } bitmap = Bitmap.createBitmap(bitmap, x, y, bmpWidth, bmpHight); bitmap = MyBitMap.createScaledBitmap(bitmap, dstWidth, dstHeight, ScaleType.FIT_XY); return bitmap; } private OnTouchListener ImageOpenListener = new OnTouchListener() {//移动监听 int lastX; int lastY; int left;//图片的左边界的坐标 int right;//图片右边界坐标 int top;//图片上边界的坐标 int bottom;//图片下边界的坐标 @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub switch(event.getAction()){ case MotionEvent.ACTION_DOWN: lastX = (int)event.getRawX(); lastY = (int)event.getRawY(); break; case MotionEvent.ACTION_MOVE: int dx = (int)event.getRawX() - lastX;//dx为在屏幕的x轴上移动的距离 int dy = (int)event.getRawY() - lastY;//dy为在屏幕的y轴上移动的距离 Bitmap newBitmap; //计算缩放的图片是否找出屏幕范围,如果是 if(bmpSizeWidth > screenWidth||bmpSizeHight > screenHeight){ if(bmpSizeWidth > screenWidth){ left = 0; right = screenWidth; }else{ left = v.getLeft() + dx; right = v.getRight() + dx; } if(bmpSizeHight > screenHeight){ top = 0; bottom = screenHeight; }else { top = v.getTop() + dy; bottom = v.getBottom() + dy; } if((dx > 3 || dx < -3) && (dy > 3 ||dy < -3)){//设置灵敏度,一定要设置 newBitmap = movCal(myBitmap, dx, dy); myImageView.setImageBitmap(newBitmap); } } else{//如果没有超出则移动ImageView left = v.getLeft() + dx; top = v.getTop() + dy; bottom = v.getBottom() + dy; right = v.getRight() + dx; } v.layout(left, top, right, bottom); lastX = (int)event.getRawX(); lastY = (int)event.getRawY(); break; } return true; } }; @Override protected void onPause() { // TODO Auto-generated method stub System.exit(0); super.onPause(); } @Override protected void onStop() { // TODO Auto-generated method stub System.exit(0); super.onStop(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } }
另外有一个辅助类,就是获取各类渠道图片的封装类
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffXfermode; import android.graphics.Shader.TileMode; import android.util.Base64; /** * 功能:得到原始的bitmap,就是unscaledbitmap;将得到bitmap字节流 * @author ZhangJianLin * */ public class MyBitmapFactory { Context context; public MyBitmapFactory(Context context) { // TODO Auto-generated constructor stub this.context = context; } public Bitmap getFileBmp(String path){//通过路径获得图片 Bitmap bm = BitmapFactory.decodeFile(path); return bm; } public Bitmap getDrawBmp(int id){//通过本项目id获得图片 Bitmap bm = BitmapFactory.decodeResource(context.getResources(), id); return bm; } public Bitmap getStringBmp(InputStream inputstring){//从流中获取图片 Bitmap bm = BitmapFactory.decodeStream(inputstring); return bm; } public Bitmap getArrayBmp(byte[] data, int offset, int length){//从字节转化成图片 Bitmap bm = BitmapFactory.decodeByteArray(data, offset, length); return bm; } //获得带倒影的图片方法 public static Bitmap createReflectionImageWithOrigin(Bitmap bitmap){ final int reflectionGap = 4; int width = bitmap.getWidth(); int height = bitmap.getHeight(); Matrix matrix = new Matrix(); matrix.preScale(1, -1); Bitmap reflectionImage = Bitmap.createBitmap(bitmap, 0, height/2, width, height/2, matrix, false); Bitmap bitmapWithReflection = Bitmap.createBitmap(width, (height + height/2), Config.ARGB_8888); Canvas canvas = new Canvas(bitmapWithReflection); canvas.drawBitmap(bitmap, 0, 0, null); Paint deafalutPaint = new Paint(); canvas.drawRect(0, height,width,height + reflectionGap, deafalutPaint); canvas.drawBitmap(reflectionImage, 0, height + reflectionGap, null); Paint paint = new Paint(); LinearGradient shader = new LinearGradient(0, bitmap.getHeight(), 0, bitmapWithReflection.getHeight() + reflectionGap, 0x70ffffff, 0x00ffffff, TileMode.CLAMP); paint.setShader(shader); // Set the Transfer mode to be porter duff and destination in paint.setXfermode(new PorterDuffXfermode(Mode.DST_IN)); // Draw a rectangle using the paint with our linear gradient canvas.drawRect(0, height, width, bitmapWithReflection.getHeight() + reflectionGap, paint); return bitmapWithReflection; } public Bitmap stringtoBitmap(String string){//从string到bitmap //将字符串转换成Bitmap类型 Bitmap bitmap=null; try { byte[]bitmapArray; bitmapArray=Base64.decode(string, Base64.DEFAULT); bitmap=BitmapFactory.decodeByteArray(bitmapArray, 0, bitmapArray.length); } catch (Exception e) { e.printStackTrace(); } return bitmap; } public String bitmaptoString(Bitmap bitmap){ //将Bitmap转换成字符串 String string=null; ByteArrayOutputStream bStream=new ByteArrayOutputStream(); bitmap.compress(CompressFormat.PNG,100,bStream); byte[]bytes=bStream.toByteArray(); string=Base64.encodeToString(bytes,Base64.DEFAULT); return string; } public Bitmap returnBitMap(String url) {//从网络中获得图片 URL myFileUrl = null; Bitmap bitmap = null; try { myFileUrl = new URL(url); } catch (MalformedURLException e) { e.printStackTrace(); } try { HttpURLConnection conn = (HttpURLConnection) myFileUrl .openConnection(); conn.setDoInput(true); conn.connect(); InputStream is = conn.getInputStream(); bitmap = BitmapFactory.decodeStream(is); is.close(); } catch (IOException e) { e.printStackTrace(); } return bitmap; } }
本实验中所用到的布局如下
<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" tools:context=".MainActivity" > <ImageView android:id="@+id/bitmap_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:src="@drawable/ic_launcher" /> <LinearLayout android:id="@+id/bitmap_button" android:layout_width="80dp" android:layout_height="40dp" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_marginBottom="16dp" android:layout_marginLeft="14dp" android:orientation="horizontal" > <Button android:id="@+id/button_small" android:layout_width="40dp" android:layout_height="40dp" android:background="@drawable/bitmap_small" /> <Button android:id="@+id/button_big" android:layout_width="40dp" android:layout_height="40dp" android:background="@drawable/bitmap_big" /> </LinearLayout> </RelativeLayout>
效果如下图
原图:
放大后
本实验有如下缺陷:(很容易改进)
1、移动时给用户的体验还不错。有一点点缺陷
2、放大时image边上有空白,这是因为计算截图时的误差
改进思路:
对于一、改进移动时的x,y及每次移动的算法规则;每次移动的距离为屏幕的1/m,提高m值,或用算法动态改变m值
对于二、当放大后的图像的宽高都大于屏幕的宽高,截获放大后的图设为全屏背景。
源码地址http://pan.baidu.com/share/link?shareid=480398&uk=2065228996,编码是utf-8