ClipView拍照截图(支持图片放大缩小移动)

来源:互联网 发布:淘宝毕业论文降重 编辑:程序博客网 时间:2024/05/16 08:16

这个截图的功能已经使用很久了,一直没有找到合适的时间来写一下。遇到最多的场景是版本1的时候产品说,这个功能很重要,然后被逼着写出来了。然后到了版本2,产品又说,现在有新的想法了,之前的功能不需要了。有种吐血的冲动。。。让我更加觉得有写出来的必要了,也算是纪念一下,那些年,被逼着造轮子的日子。

先说一下写这个功能的原因,写这个功能以前,遇到拍照截图的功能,直接调用的系统源生功能,而且基本上也够用,对于要求严格的可能就不合适了,因为源生的有一个最大的问题是,三星手机拍照后,截图时图片被翻转了90度(或许可以拍照后保存时就先翻转90度也可以解决,没有尝试过)。另外如果你也遇到了我这样的需求,产品想要自定义截图的UI,并且截图区域不动,可以拖动下面的图片来放大缩小移动等等。用过源生的都知道,android的是图片不动,截图框可以拖动。

自定义效果图以及源生的效果:

自定义效果:



系统自带效果:


截图后效果:


实现思路:

1.拍照后图片的读取以及压缩

2.截图分为上下两层

下面是图片(图片可以被放大缩小移动)

上面是一个自定义View蒙板,可以通过画笔绘制出截图区域。

3.图片拖动时,边界限制

如果图片在边框内,则图片不能被拖出截图区域。

如果图片被放大后,则图片的边缘不能拖进截图区域(为了保证截图不留白)


4.截图区域的图片扣取

a.截屏

b.计算状态栏及标题栏高度,进而计算出选框位置

c.从截屏上抠出想要的区域。


代码分析:

1.对于图片的保存及读取,写了一个Util类,里面的方法很多,足够写一篇压缩文章了,就不细说了。

2.截图UI

a. ClipView

ClipView是一个自定义View,作用是绘制遮罩及选择框。

主要是onDraw方法,绘制四周的黑色透明遮罩,然后绘制选框的边框。(画笔的使用请自行google)

@Overrideprotected void onDraw(Canvas canvas){super.onDraw(canvas);int width = this.getWidth();int height = this.getHeight();Paint paint = new Paint();paint.setColor(Util.ClipOutColor);float x = Util.getClipX(getContext());//截图区域距离左边的距离float y = Util.getClipY(getContext());//截图区域距离顶部的距离float a = Util.getClipWidth(getContext()); //截图区域的边长canvas.drawRect(0, 0, width, y, paint);canvas.drawRect(0, y, x, height, paint);canvas.drawRect(x, a+y, width, height, paint);canvas.drawRect(x+a, y, width, a+y, paint);paint.setColor(Util.ClipColor);int m = 4;canvas.drawRect(x-m,y-m,x+m+a,y,paint);canvas.drawRect(x-m,y,x,y+m+a,paint);canvas.drawRect(x,y+a,x+a,y+a+m,paint);canvas.drawRect(x+a,y,x+a+m,y+a+m,paint);}

b. 图片的缩放及位移。

实现方案有两种:

 * 矩阵变换:http://www.cnblogs.com/plokmju/p/android_Matrix.html

* layout方法:主要是通过设置View的左上右下四个位置来控制View的大小及位置,本文也是采用的这种方法。

其源码如下:

    /**     * Assign a size and position to a view and all of its     * descendants     *     * <p>This is the second phase of the layout mechanism.     * (The first is measuring). In this phase, each parent calls     * layout on all of its children to position them.     * This is typically done using the child measurements     * that were stored in the measure pass().</p>     *     * <p>Derived classes should not override this method.     * Derived classes with children should override     * onLayout. In that method, they should     * call layout on each of their children.</p>     *     * @param l Left position, relative to parent     * @param t Top position, relative to parent     * @param r Right position, relative to parent     * @param b Bottom position, relative to parent     */    @SuppressWarnings({"unchecked"})    public void layout(int l, int t, int r, int b) {        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;        }        int oldL = mLeft;        int oldT = mTop;        int oldB = mBottom;        int oldR = mRight;        boolean changed = isLayoutModeOptical(mParent) ?                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {            onLayout(changed, l, t, r, b);            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;            ListenerInfo li = mListenerInfo;            if (li != null && li.mOnLayoutChangeListeners != null) {                ArrayList<OnLayoutChangeListener> listenersCopy =                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();                int numListeners = listenersCopy.size();                for (int i = 0; i < numListeners; ++i) {                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);                }            }        }        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;    }


采用方法二,那么实现图片的缩放就很简单了,只需要获取图片原始的左上右下的距离,然后如果想缩小图片,只需要缩小左右/上下的距离即可。位移,只需要保持上下/左右的距离不变,然后改变距离左边或顶部的距离即可。

然后就是监听手势来做相应改变即可。

手势监听方法:onTouch

public boolean onTouch(View v, MotionEvent event) {switch (v.getId()){case R.id.iv_photo:switch (event.getActionMasked()){<span style="background-color: rgb(51, 204, 0);">case MotionEvent.ACTION_DOWN:</span>currentStatus = STATUS_MOVE;// 得到imageView最开始的各顶点的坐标l = iv_photo.getLeft();r = iv_photo.getRight();t = iv_photo.getTop();b = iv_photo.getBottom();width = r - l;height = b - t;Log.i("w_h","l:"+l+" t:"+t);if(widthStart == -1 && heightStart == -1){widthStart = width;heightStart = height;Log.i("w_h", "widthStart:"+widthStart+"  heightStart:"+heightStart);Log.i("w_h", "a:"+a+"  x:"+x);}startx = event.getRawX();starty = event.getRawY();Log.i("move", "startx:"+startx+"   starty:"+starty);break;<span style="background-color: rgb(51, 204, 0);">case MotionEvent.ACTION_POINTER_DOWN:</span>if(event.getPointerCount() == 1){currentStatus = STATUS_MOVE;}else if (event.getPointerCount() == 2) {currentStatus = STATUS_ZOOM;disStart = distanceBetweenFingers(event);}break;<span style="background-color: rgb(51, 204, 0);">case MotionEvent.ACTION_MOVE:</span>Log.i("state", "" + currentStatus);if (currentStatus == STATUS_MOVE && event.getPointerCount() == 1) {actionMove(event);} else if (currentStatus == STATUS_ZOOM && event.getPointerCount() == 2) {actionZoom(event);}break;<span style="background-color: rgb(51, 204, 0);">case MotionEvent.ACTION_UP:</span>if(currentStatus == STATUS_ZOOM) {// 得到imageView最开始的各顶点的坐标l = iv_photo.getLeft();r = iv_photo.getRight();t = iv_photo.getTop();b = iv_photo.getBottom();width = r - l;height = b - t;if (width <= a || height <= a) {l = (int)(x+(a-width)/2);t = (int)(y+(a-height)/2);r = (int)(x+(a+width)/2);b = (int)(y+(a+height)/2);iv_photo.layout(l,t,r,b);}}currentStatus = STATUS_INIT;break;default:break;}break;}<span style="background-color: rgb(51, 204, 0);">return true;</span>}
主要的关键点我已经标示出来了,主要就是监听用户手势按下,然后判断用户是一个手指还是两个手指。一个手指肯定是移动了,两个手指就是缩放。

其中return true比较重要,只有返回true才能实现连续监听( 这个就涉及到事件的监听机制了,有兴趣的可以了解一下)。

两个手指的位移计算:

/** * 计算两个手指之间的距离。 * * @param event * @return 两个手指之间的距离 */private double distanceBetweenFingers(MotionEvent event) {float disX = Math.abs(event.getX(0) - event.getX(1));float disY = Math.abs(event.getY(0) - event.getY(1));return Math.sqrt(disX * disX + disY * disY);}


缩放的方法:

通过判断两个手指之间的距离与原始距离的比例,来计算图片需要缩放的大小。

/** * 缩放 * @param event */private void actionZoom(MotionEvent event) {disMove = distanceBetweenFingers(event);double scale = disMove/disStart;double scaleTemp = (width * scale)/widthStart;double minScale = 0.5;if(a < heightStart) {minScale = a / heightStart;}else{minScale = 1;}Log.i("zoom", "scaleTemp:"+scaleTemp);if(scaleTemp > 2){scale = 2*widthStart/(width);}else if(scaleTemp < minScale){scale = minScale*widthStart/(width);}Log.i("scale", ""+scale);double dw = width*(scale -1);double dh = height*(scale -1);int lm = (int)(l - dw/2);int rm = (int)(r + dw/2);int tm = (int)(t - dh/2);int bm = (int)(b + dh/2);if(heightStart > a && bm - tm <a){bm +=1;tm -=1;}iv_photo.layout(lm, tm, rm, bm);}


位移

位置的移动比较简单,难点在于如何限制移动的边界。

边界判断可以根据坐标来判断,首先判断图片比选框小,还是比选框大。

如果比选框小,则只需要保证图片的左侧大于x,右侧小于x + a即可。y轴方法同x。

如果比选框大,则只需要保证图片的左侧小于x,右侧大于x + a即可。y同x。

/** * 移动 * @param event */private void actionMove(MotionEvent event) {int x1 = (int) event.getRawX();int y1 = (int) event.getRawY();Log.i("move", "x1:"+x1+"    y1:"+y1);// 获取手指移动的距离int dx = (int) (x1 - startx);int dy = (int) (y1 - starty);if(width > a){if(l+dx >= x){dx = x - l;}if(r+dx <= x +a){dx = x + a - r;}}else{if(l+dx <= x){dx = x - l;}if(r+dx >= x +a){dx = x + a - r;}}if(height >a){if(t+dy >= y){dy = y - t;}if(b+dy <= y + a){dy = y + a - b;}}else{if(t+dy <= y){dy = y - t;}if(b+dy >= y + a){dy = y + a - b;}}iv_photo.layout(l+dx, t+dy, r+dx, b+dy);}

3.缩放和位移搞定了,下面就是获取选框区域的图片了。

我的思路是首选把整个屏幕的图片获取到,然后计算选框的位置,截取相应的位置图片即可。

a.获取屏幕截图:

// 获取Activity的截屏private Bitmap takeScreenShot() {View view = this.getWindow().getDecorView();view.setDrawingCacheEnabled(true);view.buildDrawingCache();return view.getDrawingCache();}

b.计算截图区域

需要使用createBitmap方法:

    /**     * Returns an immutable bitmap from the specified subset of the source     * bitmap. The new bitmap may be the same object as source, or a copy may     * have been made. It is initialized with the same density as the original     * bitmap.     *     * @param source   The bitmap we are subsetting     * @param x        The x coordinate of the first pixel in source     * @param y        The y coordinate of the first pixel in source     * @param width    The number of pixels in each row     * @param height   The number of rows     * @return A copy of a subset of the source bitmap or the source bitmap itself.     * @throws IllegalArgumentException if the x, y, width, height values are     *         outside of the dimensions of the source bitmap, or width is <= 0,     *         or height is <= 0     */    public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) {        return createBitmap(source, x, y, width, height, null, false);    }
因此,我们只需要计算出选框的左上角坐标即可,width,height都是a(选框变长)。x就是开始的x。 y的计算需要考虑状态栏的高度和标题栏的高度(因为截屏截取到的是整个屏幕)。


状态栏和标题栏获取:

int statusBarHeight = 0;int titleBarHeight = 0;private void getBarHeight() {// 获取状态栏高度Rect frame = new Rect();getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);statusBarHeight = frame.top;int contenttop = this.getWindow().findViewById(Window.ID_ANDROID_CONTENT).getTop();// statusBarHeight是上面所求的状态栏的高度titleBarHeight = contenttop - statusBarHeight;if(titleBarHeight < 0) titleBarHeight = 0;Log.v("bar", "statusBarHeight = " + statusBarHeight + ", titleBarHeight = " + titleBarHeight);}

:这里有一个问题,获取到的titleBarHeight不准确(具体原因不清楚),所以此demo只支持全屏或者只有状态栏的情况,否则会导致截图区域计算错误。(这个需要各位大神来指点了)

4.截屏及压缩图片:

/* 获取矩形区域内的截图 */private Bitmap getBitmap() {getBarHeight();Bitmap screenShoot = null;screenShoot = takeScreenShot();Bitmap finalBitmap = Bitmap.createBitmap(screenShoot,x  + 1 , //x轴方向起点y  + 1 + titleBarHeight + statusBarHeight,//y轴方向起点a  - 1, //截取的宽度a  - 1  //截取的高度);return imageZoom(finalBitmap, 200);}private Bitmap imageZoom(Bitmap bitMap, double size) {//图片允许最大空间   单位:KB//将bitmap放至数组中,意在bitmap的大小(与实际读取的原文件要大)ByteArrayOutputStream baos = new ByteArrayOutputStream();bitMap.compress(Bitmap.CompressFormat.JPEG, 100, baos);byte[] b = baos.toByteArray();//将字节换成KBdouble mid = b.length/1024;//判断bitmap占用空间是否大于允许最大空间  如果大于则压缩 小于则不压缩if (mid > size) {//获取bitmap大小 是允许最大大小的多少倍double i = mid / size;//开始压缩  此处用到平方根 将宽带和高度压缩掉对应的平方根倍 (1.保持刻度和高度和原bitmap比率一致,压缩后也达到了最大大小占用空间的大小)//bitMap = zoomImage(bitMap, bitMap.getWidth() / Math.sqrt(i),//bitMap.getHeight() / Math.sqrt(i));bitMap = zoomImage(bitMap, 800, 800);}return bitMap;}/*** * 图片的缩放方法 * * @param bgimage *            :源图片资源 * @param newWidth *            :缩放后宽度 * @param newHeight *            :缩放后高度 * @return */public static Bitmap zoomImage(Bitmap bgimage, double newWidth,   double newHeight) {// 获取这个图片的宽和高float width = bgimage.getWidth();float height = bgimage.getHeight();// 创建操作图片用的matrix对象Matrix matrix = new Matrix();// 计算宽高缩放率float scaleWidth = ((float) newWidth) / width;float scaleHeight = ((float) newHeight) / height;// 缩放图片动作matrix.postScale(scaleWidth, scaleHeight);Bitmap bitmap = Bitmap.createBitmap(bgimage, 0, 0, (int) width,(int) height, matrix, true);int w = bitmap.getWidth();int h = bitmap.getHeight();Log.i("wh", "w"+w+" h"+h);return bitmap;}

配置信息:写在了Util类中(懒的写了,应该写一个自定义属性的ClipView的,这个可以参考上一篇文章)

    public static final int x = 15;//截图区域左上角x坐标    public static final int y = 138;//截图区域左上角y坐标    public static final int ClipOutColor = 0xb3000000;//截图外围颜色    public static final int ClipColor = 0xb32d3a60;//截图区域颜色    public static int getClipX(Context context){        return dip2px(x, context);    }    public static int getClipY(Context context){        return dip2px(y, context);    }    public static int getClipWidth(Context context){        return getWidthPx(context) - dip2px(x, context) * 2;    }

里面的细节确实太多,所以只是写了一个大概的思路,详细的源码可以从下面下载。

源码地址:https://github.com/736791050/ClipView

0 0