自定义ImageView实现局部截图功能

1、前言

最近在做一个能够自选区域进行局部截图的功能,接下来,会给大家讲解,整个截图的实现过程。笔者这边实现的自选区域的形状是矩形,读者如果有需要,可以根据我给大家讲解的思路,修改成适合自己的截图工具。先来看看效果图

2、效果图

这里的图片是来自笔者对webView的截图产生的,读者可以根据自己的需要,替换上面的图片。

通过拖拽四条边框,可以实现屏幕的局部截图:

拖拽之后,只有需要截图的部分才会高亮显示,其余部分用遮罩掩盖。笔者实现的拖拽四条边都可以任意拖拽,并不一定要正方形或者长方形。也可以如下:

3、截图的基本知识

实现截图功能之前,我们需要了解View为我们提供的截图方法的使用。笔者会尽量详细讲述各个方法,让读者可以比较明白的去自由结合各截图方法的使用。

3.1、截图的基本知识点

在对View进行截图的时候,系统需要将当前View的内容缓存下来,因此,在截图之前,我们需要判断当前View是否允许视图缓存。

public boolean isDrawingCacheEnabled()

如果当前View允许进行视图缓存,会返回true否则返回false。如果返回false,我们就需要设置当前的视图,让它允许视图缓存。

 public void setDrawingCacheEnabled(boolean enabled)

通过设置true来设置当前View,让它可以进行视图缓存。当设置为true,那么接下来调用getDrawingCache或者buildDrawingCache方法,就会给当前视图的内容生成一个bitmap图像。

给当前视图强制生成缓存

public void buildDrawingCache(boolean autoScale)

此方法在视图不可缓存的时候,会强制生成一个视图缓存,autoScale表示是否自适应缩放缓存视图,如果为false的生成的视图大小和View的大小是一致的,true的话,则会根据内容生成自适应大小的图片。但是如果在调用此方法的之前,你没有调用setDrawingEnabled(true)的话,在对生成的视图使用完毕之后,应该对强制生成的视图缓存进行一个清理。清理的方法如下:

public void destroyDrawingCache()

此方法会将缓存的视图内容进行清理,从而对bitmap的资源进行释放回收利用。

获取缓存图像的方法:

public Bitmap getDrawingCache(boolean autoScale)

此方法会将View的视图缓存生成一个bitmap对象并返回,这样我们就可以对view的内容进行截图了。autoscale参数和前面讲述的功能一样。

综合上述知识,使用方法如下:

 if(!webView.isDrawingCacheEnabled())
             webView.setDrawingCacheEnabled(true);
        webView.buildDrawingCache();
        Bitmap cropBitmap = webView.getDrawingCache(true);

4、自定义View的知识点要求

由于我们将会对View的视图内容进行部分截图,所以需要我们自定义一个能够监听用户动作从而产生对应响应的View。关于自定义View的基础知识,读者如果不明白的,可以看笔者的这篇文章自定义View的基本思路,如果读者有这些基础知识,那么可以接着往下看。

先给一个下面讲解会遇到的局部变量和常量的含义说明,这样会比较容易理解下面的操作逻辑:

//移动过程的xy,imageView的canvas的宽高,点击事件的宽高,源图片的宽高。
    private float moveX,moveY,startWidth,startHeight,downX,downY,bitmapWidth,bitmapHeight;
    private Context context;
    //当前手机的像素密度
    private float density;
    //是否可以开始拖拽选择框
    private boolean isStart=true;
    //分别表示上线,下线,左线,右线
    private Line upLine,downLine,leftLine,rightLine;
    //是否是上线移动,是否是垂直移动,是否是左线移动,是否是水平移动
    //垂直移动分为上线移动和下线移动,垂直移动同理
    private boolean isMovingUpLine=false,isMovingVertical=false,isMovingLeftLine=false,isMovingHorizontal=false;
   //用来给点击点的坐标预留一些空间,便于判断移动时间的判断
    private float padding;
    private Paint paint = new Paint();

笔者将通过实现一个自定义ImageView,来实现一个可拖拽矩形边框,对选中内容高亮显示,对不选的内容进行遮罩掩饰。由于需要监听用户的触摸行为,我们需要实现OnTouchListener接口,如下:

public class ClipImageView extends ImageView implements View.OnTouchListener

当ClipImageView第一次展示的时候,我们需要在它的四周画上矩形边框,如图:

可以看到,在图片和四周的边框之间是有一定的留白的,便于分辨边框和图片的部分。所以我们需要重写OnDraw方法:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(isStart&&moveY==0) {
            //初次布局,绘画四周的边线
            initCanvas(canvas);

        }
        else
        {
            //重画边线和遮罩
            reDraw(canvas);
        }
    }

我们关注iniCanvas方法即可,当前imageView如果是刚刚显示出来并且没有用户触摸动作的产生,就绘制四周的边框。为什么需要判断是否有用户触摸动作呢,因为用户产生触摸动作的时候,View一定是处于可见生命周期,此时就不是第一次绘制视图了,而是需要响应用户触摸行为来绘制视图了。

 /**
     * 初始化布局
     * @param canvas
     */
    private void initCanvas(Canvas canvas) {
        //获取画布的宽高,用于初始化边线的宽高
        startWidth = canvas.getWidth();
        startHeight = canvas.getHeight();
        upLine=new Line(0,0,startWidth,0);
        downLine=new Line(0, startHeight, startWidth, startHeight);
        leftLine=new Line(0,0,0,startHeight);
        rightLine=new Line(startWidth,0,startWidth,startHeight);
        Log.i("上线坐标",upLine.getLeft()+" "+upLine.getTop()+" "+upLine.getRight()+" "+upLine.getBottom() );
        Log.i("下线坐标",downLine.getLeft()+" "+downLine.getTop()+" "+downLine.getRight()+" "+downLine.getBottom() );
        Log.i("左线坐标",leftLine.getLeft()+" "+leftLine.getTop()+" "+leftLine.getRight()+" "+leftLine.getBottom());
        Log.i("右线坐标",rightLine.getLeft()+" "+rightLine.getTop()+" "+rightLine.getRight()+" "+rightLine.getBottom());
        //画出上下左右四条线
        paint.setStrokeWidth(4);
        paint.setColor(Color.WHITE);
        //0-255,越小越透明
        paint.setAlpha(255);
        //画边线
        canvas.drawLine(0, 0, startWidth, 0, paint);//上
        canvas.drawLine(0, startHeight, startWidth, startHeight, paint);//下
        canvas.drawLine(0,0,0,startHeight,paint);//左
        canvas.drawLine(startWidth,0,startWidth,startHeight,paint);//右
    }

上述使用了paint和canvas进行绘制边框,不懂得可以看我上面提到的博客。这里面的Line是一个笔者自定义的bean对象,只是为了方便对用户触摸动作进行判断产生的。如下:

package cn.com.chinaweal.share;

/**
 * Created by Myy on 2016/8/27.
 */
public class Line {

    private float left;//startX
    private float top;//startY
    private float right;//stopX
    private float bottom;//stopY

    public Line(float left, float top, float right, float bottom) {
        this.left = left;
        this.top = top;
        this.right = right;
        this.bottom = bottom;
    }

    public float getLeft() {
        return left;
    }

    public void setLeft(float left) {
        this.left = left;
    }

    public float getTop() {
        return top;
    }

    public void setTop(float top) {
        this.top = top;
    }

    public float getRight() {
        return right;
    }

    public void setRight(float right) {
        this.right = right;
    }

    public float getBottom() {
        return bottom;
    }

    public void setBottom(float bottom) {
        this.bottom = bottom;
    }
}

上面的解释其实解释为起始点和终止点更合适,因为它主要是用来记录一条线的起点和重点的坐标的。

当我们绘制好边框之后,就需要监听用户的触摸行为了,这也是实现可拖拽ImageView最大的难点。里面涉及的逻辑思维,是笔者通过调试一步一步得出来的,读者如果不懂没关系,可以自己按照自己的行为一步一步调试。代码如下:

 /**
     * 监听触摸事件,这里的逻辑是最复杂的
     * @param v
     * @param event
     * @return
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(event.getAction()==MotionEvent.ACTION_DOWN)
        {
            //记录当前点击的坐标,用于判断用户当前移动的是那条线以及用于产生移动距离
            downX=event.getX();
            downY=event.getY();
            //说明当前点击的是上线,波动距离为上线上下20个像素点的范围
            if((downY<=upLine.getTop()+padding)&&(downY>=upLine.getTop()-padding))
            {
                isMovingUpLine=true;//表示当前是上线被用户拖拽
                isMovingVertical=true;//表示是垂直方向的移动
                isMovingHorizontal=false;//表示不是水平方向的移动
            }
            else if((downY<=downLine.getTop()+padding)&&(downY>=downLine.getTop()-padding))//下线
            {
                isMovingUpLine=false;
                isMovingVertical=true;
                isMovingHorizontal=false;
            }
            else if((downX<=leftLine.getLeft()+padding)&&(downX>=leftLine.getLeft()-padding))//左线
            {
                isMovingLeftLine=true;
                isMovingHorizontal=true;
                isMovingVertical=false;
            }
            else if((downX<=rightLine.getLeft()+padding)&&(downX>=rightLine.getLeft()-padding))//右线
            {
                isMovingLeftLine=false;
                isMovingHorizontal=true;
                isMovingVertical=false;
            }
            else
            {
                //如果不在任何一条边线的拖动范围,对用户此次触摸事件忽略
                isMovingHorizontal=false;
                isMovingVertical=false;
            }
            return  true;
        }
        if(event.getAction()==MotionEvent.ACTION_MOVE)
        {
            //产生移动距离,用于重新绘制边线和遮罩
            moveX=event.getX()-downX;
            moveY=event.getY()-downY;
            //必须保存这次的移动坐标,以便下一次移动的时候可以判断从上一次的移动坐标产生新的移动的距离
            downX=event.getX();
            downY=event.getY();
            if(isMovingUpLine&&isMovingVertical)
            {
                float detY=upLine.getTop()+moveY;
                //当移动的距离超过下线20个像素范围或到达顶端的时候,需要进行特殊的处理。
                //否则就使用移动的距离。
                //上线必须处于下线上边的20个像素点以上,避免交叉
                if (detY>=downLine.getTop()-padding)
                    detY=downLine.getTop()-padding;
                //上线不可超过顶端
                if(detY<=0)
                    detY=0;
                upLine.setTop(detY);
                upLine.setBottom(detY);
            }
            if(!isMovingUpLine&&isMovingVertical)
            {
                //下线的处理
                float detY=downLine.getTop()+moveY;
                if(detY<=upLine.getTop()+padding)//和上线保持20的像素点
                    detY=upLine.getTop()+padding;
                if(detY>=startHeight)//不可超过边线
                    detY=startHeight;
                downLine.setTop(detY);
                downLine.setBottom(detY);
            }
            if(isMovingLeftLine&&isMovingHorizontal)
            {
                //左线的处理
                float detX=leftLine.getLeft()+moveX;
                if (detX>=rightLine.getLeft()-padding)
                    detX=rightLine.getLeft()-padding;
                if(detX<=0)
                    detX=0;
                leftLine.setLeft(detX);
                leftLine.setRight(detX);
            }
            if(!isMovingLeftLine&&isMovingHorizontal)
            {
                //右线的处理
                float detX=rightLine.getLeft()+moveX;
                if(detX<=leftLine.getLeft()+padding)//和上线保持20的像素点
                    detX=leftLine.getLeft()+padding;
                if(detX>=startWidth)//不可超过边线
                    detX=startWidth;
                rightLine.setLeft(detX);
                rightLine.setRight(detX);
            }
            if(isMovingVertical||isMovingHorizontal)
               invalidate();
            Log.i("moveY",moveY+"");
            Log.i("isMovingVertical",isMovingVertical+"");
            Log.i("isMovingHorizontal",isMovingHorizontal+"");
            Log.i("上线",isMovingUpLine+"");
            Log.i("moveX",moveX+"");
            Log.i("左线",isMovingLeftLine+"");
            return true;
        }

        if(event.getAction()==MotionEvent.ACTION_UP)
        {

            return true;
        }
        return false;
    }

上述的触摸逻辑比较复杂,笔者在注释讲了很清楚了,因为不涉及技术难点,所以就不展开讲解,逻辑的疑惑还需要读者自行去调试解惑。注意到,我们有在某个特点条件下调用了invalidate方法,这个方法是干嘛的呢?

此方法会导致当前View变得不可用,然后就会调用onDraw重新绘制View。此方法是运行在主线程中的,请注意,所以不要在onDraw里面做耗时操作。其实不只是onDraw,任何View的方法都不应该进行耗时操作。android还提供了另外一个可以产生相同行为的方法,不同点是,它的执行实在非主线程中的,叫做postInvalidate。

ClipImageView的onDraw方法在前面提到过,现在要了解的是reDraw方法,此方法用于相应用户触摸行为,来重新绘制边框的线和遮罩。如下:

方法如下:

  /**
     * 重新绘制矩形区域
     * @param canvas
     */
    private void reDraw(Canvas canvas) {
        isStart=false;
        paint.setStrokeWidth(4);
        //style有三种模式fill,stroke,fill_and_stroke三种。
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setColor(Color.WHITE);
        //画出上线和下线左线右线
        //画上线的时候,应该以左线作为x的起点的参考,以右线作为x的终点的参考。其余线都需要参考其他线,逻辑不列了
        canvas.drawLine(leftLine.getLeft(),upLine.getTop(),rightLine.getLeft(),upLine.getBottom(),paint);//上线
        canvas.drawLine(leftLine.getLeft(),downLine.getTop(),rightLine.getRight(),downLine.getBottom(),paint);//下线
        canvas.drawLine(leftLine.getLeft(),upLine.getTop(),leftLine.getRight(),downLine.getBottom(),paint);//左线
        canvas.drawLine(rightLine.getLeft(),upLine.getTop(),rightLine.getRight(),downLine.getBottom(),paint);//右线
        //画出遮罩
        paint.setColor(Color.BLACK);
        //设置透明度,以至于下边的内容不会完全被遮住导致不可见。
        paint.setAlpha(180);
        canvas.drawRect(0,0,upLine.getRight(),upLine.getBottom()-1,paint);//上边遮罩
        canvas.drawRect(downLine.getLeft(),downLine.getTop()+1,startWidth,startHeight,paint);//下边遮罩
        //画左右遮罩需要照顾到上下遮罩,避免重新绘制已经存在遮罩的区域
        //3为的是消除遮罩之间的缝隙
        canvas.drawRect(0,upLine.getTop()+3,leftLine.getRight()-1,downLine.getTop()-3,paint);//左边遮罩
        canvas.drawRect(rightLine.getLeft()+1,upLine.getTop()+3,startWidth,downLine.getTop()-3,paint);//右边遮罩
        Log.i("上线坐标",upLine.getLeft()+" "+upLine.getTop()+" "+upLine.getRight()+" "+upLine.getBottom() );
        Log.i("下线坐标",downLine.getLeft()+" "+downLine.getTop()+" "+downLine.getRight()+" "+downLine.getBottom() );
        Log.i("左线坐标",leftLine.getLeft()+" "+leftLine.getTop()+" "+leftLine.getRight()+" "+leftLine.getBottom());
        Log.i("右线坐标",rightLine.getLeft()+" "+rightLine.getTop()+" "+rightLine.getRight()+" "+rightLine.getBottom());
    }

难点在于了解遮罩的绘画逻辑和边框的逻辑,其实在我们绘制上线的时候,x的起点坐标,必须和左线的x的坐标相同,这样才能形成一个矩形区域。挥着遮罩的时候,我们把上半部分的遮罩全部参照上线为界限,下半部分参照下线为界限。而左边部分,就以左线的X左边到canvas的边线为宽,高度则是以上线的y和下线的y为参考,这样就不会导致四个角落被重复遮罩了。这样说很晦涩难懂,读者可以想象一下,如果不宜上线和下线为参考标准,而是以左线的xy会参考标准,是否会导致左上角和左下角的区域被遮罩绘制依次。而绘制上半部分的遮罩的时候,以上线的xy为标准,那么就会绘制左上角有右上角的部分,这样就导致左上角被绘制了两次,从而整个画面不协调。

到了这里,基本实现了一个可拖拽的自由选择的矩形区域的ImageView。那么接下来,我们就绪截取部分图片从而产生用户所期望的部分截图了。

5、截取部分图案

我们需要对Bitmap对象进行部分截取图案,需要了解下面这个方法:

public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) 

source表示图片源,x,y表示截图的起始坐标,width,height表示截取的宽高。

因此,如果我们需要对用户选中的图案进行截图,就需要获取起始坐标和宽高。但因为,我们自定义的imagView的显示的图片的大小和显示的bitmap源图片的大小并不一致,因此我们需要对它进行一个比率转换,将用户选中的区域转换成源bitmap的真实区域。代码如下:

 /**
     * 获取截图的范围
     * @return
     */
    public Line getClipLine()
    {
        Bitmap bitmap=((ClipActivity)context).getBitmap();
        bitmapWidth=bitmap.getWidth();//获取源图片的宽高
        bitmapHeight=bitmap.getHeight();

        //计算当前显示的图片和原始图片的缩放比例
        float xRatio=bitmapWidth/(startWidth-getPaddingRight()-getPaddingLeft());
        float yRatio=bitmapHeight/(startHeight-getPaddingTop()-getPaddingBottom());
        //判断截图的XY坐标,如果小于0,则认为截取的图片XY是从0开始
        float clipX=(leftLine.getLeft()-getPaddingLeft())<=0?0:leftLine.getLeft()-getPaddingLeft();
        float clipY=(upLine.getTop()-getPaddingTop())<=0?0:upLine.getTop()-getPaddingTop();
        //获得截取的图片XY转换成原始图片的XY坐标
        float x=clipX*xRatio;
        float y=clipY*yRatio;
        //获得截取图片的宽高
        float clipWidth=(rightLine.getRight()-(startWidth-getPaddingRight())>=0?startWidth-getPaddingRight()-getPaddingLeft():rightLine.getRight())-clipX;
        float clipHeight=(downLine.getBottom()-(startHeight-getPaddingBottom())>=0?startHeight-getPaddingBottom()-getPaddingTop():downLine.getBottom())-clipY;
        //准换成原始图片的宽高
        float width=clipWidth*xRatio;
        float height=clipHeight*yRatio;

        Line line=new Line(x,y,width,height);
        return line;
    }

上述比较重要的是,对于周边padding部分的忽略,因为用于截取的图片是在图片区域内,并不包含外部缝隙。而我们为了方便描绘边框,添加了外部缝隙,所以这些要忽略掉。

上述是整个ClipImageView的实现过程。接下来,整个截图完整流程的核心代码部分,读者自行参考:

ClipActivity的资源文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/share_text">

    <LinearLayout
        android:id="@+id/line1"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="@color/clip_black"
        android:orientation="horizontal"
        android:weightSum="2">

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@color/clip_black"
            android:text="取消"
            android:onClick="cancel"
            android:textColor="@color/clip_text" />

        <Button
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:onClick="sure"
            android:background="@color/clip_black"
            android:text="确定"
            android:textColor="@color/clip_text" />

    </LinearLayout>

    <cn.com.chinaweal.share.ClipImageView
        android:padding="10dp"
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:layout_centerInParent="true"
        android:scaleType="fitXY"
        android:id="@+id/imageView"/>
</RelativeLayout>

ClipActivity:

package cn.com.chinaweal.share;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;

/**
 * 图像截图
 * Created by Myy on 2016/8/27.
 */
public class ClipActivity extends AppCompatActivity {

    private ClipImageView imageView;
    private Line line;
    private Bitmap bitmap;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.line_layout);
        imageView = (ClipImageView) findViewById(R.id.imageView);
        bitmap = BitmapFactory.decodeFile(Constants.path);
        imageView.setImageBitmap(bitmap);
    }

    public void sure(View view) {
        line = imageView.getClipLine();
        Log.i("结果", line.getLeft() + " " + line.getTop() + " " + line.getRight() + " " + line.getBottom());
        bitmap=bitmap.createBitmap(bitmap, (int)line.getLeft(),(int)line.getTop(),(int)line.getRight(),(int)line.getBottom());//截取图片
        Constants.saveBitmap(bitmap);//保存截取图片
        setResult(RESULT_OK);
        finish();
    }

    /**
     * 获取图片
     * @return
     */
    public Bitmap getBitmap()
    {
        return  bitmap;
    }

    public void cancel(View view)
    {
        finish();
    }

}

Constans:

package cn.com.chinaweal.share;

import android.graphics.Bitmap;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by Myy on 2016/8/29.
 */
public class Constants {

    public static String path="/sdcard/ic_launcher.png";//文件路径

    /**
     * 保存图片
     * @param cropBitmap
     */
    public static void saveBitmap(Bitmap cropBitmap) {
        File file = new File(Constants.path);
        FileOutputStream os=null;
        if (file.exists())
            file.delete();
        try {
            file.createNewFile();
            os = new FileOutputStream(file);
            cropBitmap.compress(Bitmap.CompressFormat.PNG, 100, os);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

clipImageView:

package cn.com.chinaweal.share;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;

/**
 * Created by Myy on 2016/8/27.
 */
public class ClipImageView extends ImageView implements View.OnTouchListener{

    //移动过程的xy,imageView的canvas的宽高,点击事件的宽高,源图片的宽高。
    private float moveX,moveY,startWidth,startHeight,downX,downY,bitmapWidth,bitmapHeight;
    private Context context;
    //当前手机的像素密度
    private float density;
    //是否可以开始拖拽选择框
    private boolean isStart=true;
    //分别表示上线,下线,左线,右线
    private Line upLine,downLine,leftLine,rightLine;
    //是否是上线移动,是否是垂直移动,是否是左线移动,是否是水平移动
    //垂直移动分为上线移动和下线移动,垂直移动同理
    private boolean isMovingUpLine=false,isMovingVertical=false,isMovingLeftLine=false,isMovingHorizontal=false;
   //用来给点击点的坐标预留一些空间,便于判断移动时间的判断
    private float padding;
    private Paint paint = new Paint();
    public ClipImageView(Context context) {
        super(context);
        init(context);
    }

    public ClipImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);

    }

    private void init(Context context)
    {
        this.context=context;
        density=ScreenUtils.getDensity(context);
        //默认事件产生的波动距离是20个像素点
        padding=density*20;
        //监听动作,产生移动事件
        this.setOnTouchListener(this);

    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(isStart&&moveY==0) {
            //初次布局,绘画四周的边线
            initCanvas(canvas);

        }
        else
        {
            //重画边线和遮罩
            reDraw(canvas);
        }
    }

    /**
     * 重新绘制矩形区域
     * @param canvas
     */
    private void reDraw(Canvas canvas) {
        isStart=false;
        paint.setStrokeWidth(4);
        //style有三种模式fill,stroke,fill_and_stroke三种。
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setColor(Color.WHITE);
        //画出上线和下线左线右线
        //画上线的时候,应该以左线作为x的起点的参考,以右线作为x的终点的参考。其余线都需要参考其他线,逻辑不列了
        canvas.drawLine(leftLine.getLeft(),upLine.getTop(),rightLine.getLeft(),upLine.getBottom(),paint);//上线
        canvas.drawLine(leftLine.getLeft(),downLine.getTop(),rightLine.getRight(),downLine.getBottom(),paint);//下线
        canvas.drawLine(leftLine.getLeft(),upLine.getTop(),leftLine.getRight(),downLine.getBottom(),paint);//左线
        canvas.drawLine(rightLine.getLeft(),upLine.getTop(),rightLine.getRight(),downLine.getBottom(),paint);//右线
        //画出遮罩
        paint.setColor(Color.BLACK);
        //设置透明度,以至于下边的内容不会完全被遮住导致不可见。
        paint.setAlpha(180);
        canvas.drawRect(0,0,upLine.getRight(),upLine.getBottom()-1,paint);//上边遮罩
        canvas.drawRect(downLine.getLeft(),downLine.getTop()+1,startWidth,startHeight,paint);//下边遮罩
        //画左右遮罩需要照顾到上下遮罩本
        //3为的是消除遮罩之间的缝隙
        canvas.drawRect(0,upLine.getTop()+3,leftLine.getRight()-1,downLine.getTop()-3,paint);//左边遮罩
        canvas.drawRect(rightLine.getLeft()+1,upLine.getTop()+3,startWidth,downLine.getTop()-3,paint);//右边遮罩
        Log.i("上线坐标",upLine.getLeft()+" "+upLine.getTop()+" "+upLine.getRight()+" "+upLine.getBottom() );
        Log.i("下线坐标",downLine.getLeft()+" "+downLine.getTop()+" "+downLine.getRight()+" "+downLine.getBottom() );
        Log.i("左线坐标",leftLine.getLeft()+" "+leftLine.getTop()+" "+leftLine.getRight()+" "+leftLine.getBottom());
        Log.i("右线坐标",rightLine.getLeft()+" "+rightLine.getTop()+" "+rightLine.getRight()+" "+rightLine.getBottom());
    }

    /**
     * 初始化布局
     * @param canvas
     */
    private void initCanvas(Canvas canvas) {
        //获取画布的宽高,用于初始化边线的距离
        startWidth = canvas.getWidth();
        startHeight = canvas.getHeight();
        upLine=new Line(0,0,startWidth,0);
        downLine=new Line(0, startHeight, startWidth, startHeight);
        leftLine=new Line(0,0,0,startHeight);
        rightLine=new Line(startWidth,0,startWidth,startHeight);
        Log.i("上线坐标",upLine.getLeft()+" "+upLine.getTop()+" "+upLine.getRight()+" "+upLine.getBottom() );
        Log.i("下线坐标",downLine.getLeft()+" "+downLine.getTop()+" "+downLine.getRight()+" "+downLine.getBottom() );
        Log.i("左线坐标",leftLine.getLeft()+" "+leftLine.getTop()+" "+leftLine.getRight()+" "+leftLine.getBottom());
        Log.i("右线坐标",rightLine.getLeft()+" "+rightLine.getTop()+" "+rightLine.getRight()+" "+rightLine.getBottom());
        //画出上下左右两条线
        paint.setStrokeWidth(4);
        paint.setColor(Color.WHITE);
        //0-255,越小越透明
        paint.setAlpha(255);
        //画边线
        canvas.drawLine(0, 0, startWidth, 0, paint);
        canvas.drawLine(0, startHeight, startWidth, startHeight, paint);
        canvas.drawLine(0,0,0,startHeight,paint);
        canvas.drawLine(startWidth,0,startWidth,startHeight,paint);
    }

    /**
     * 监听触摸事件,这里的逻辑是最复杂的
     * @param v
     * @param event
     * @return
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if(event.getAction()==MotionEvent.ACTION_DOWN)
        {
            //记录当前点击的坐标,用于判断用户当前移动的是那条线以及用于产生移动距离
            downX=event.getX();
            downY=event.getY();
            //说明当前点击的是上线,波动距离为上线上下20个像素点的范围
            if((downY<=upLine.getTop()+padding)&&(downY>=upLine.getTop()-padding))
            {
                isMovingUpLine=true;//表示当前是上线被用户拖拽
                isMovingVertical=true;//表示是垂直方向的移动
                isMovingHorizontal=false;//表示不是水平方向的移动
            }
            else if((downY<=downLine.getTop()+padding)&&(downY>=downLine.getTop()-padding))
            {
                isMovingUpLine=false;
                isMovingVertical=true;
                isMovingHorizontal=false;
            }
            else if((downX<=leftLine.getLeft()+padding)&&(downX>=leftLine.getLeft()-padding))
            {
                isMovingLeftLine=true;
                isMovingHorizontal=true;
                isMovingVertical=false;
            }
            else if((downX<=rightLine.getLeft()+padding)&&(downX>=rightLine.getLeft()-padding))
            {
                isMovingLeftLine=false;
                isMovingHorizontal=true;
                isMovingVertical=false;
            }
            else
            {
                //如果不在任何一条边线的拖动范围,对用户此次触摸事件忽略
                isMovingHorizontal=false;
                isMovingVertical=false;
            }
            return  true;
        }
        if(event.getAction()==MotionEvent.ACTION_MOVE)
        {
            //产生移动距离,用于重新绘制边线和遮罩
            moveX=event.getX()-downX;
            moveY=event.getY()-downY;
            //必须保存这次的移动坐标,以便下一次移动的时候可以判断从上一次的移动坐标产生新的移动的距离
            downX=event.getX();
            downY=event.getY();
            if(isMovingUpLine&&isMovingVertical)
            {
                float detY=upLine.getTop()+moveY;
                //当移动的距离超过下线20个像素范围或到达顶端的时候,需要进行特殊的处理。
                //否则就使用移动的距离。
                //上线必须处于下线上边的20个像素点以上
                if (detY>=downLine.getTop()-padding)
                    detY=downLine.getTop()-padding;
                //上线不可超过顶端
                if(detY<=0)
                    detY=0;
                upLine.setTop(detY);
                upLine.setBottom(detY);
            }
            if(!isMovingUpLine&&isMovingVertical)
            {
                //下线的处理
                float detY=downLine.getTop()+moveY;
                if(detY<=upLine.getTop()+padding)//和上线保持20的像素点
                    detY=upLine.getTop()+padding;
                if(detY>=startHeight)//不可超过边线
                    detY=startHeight;
                downLine.setTop(detY);
                downLine.setBottom(detY);
            }
            if(isMovingLeftLine&&isMovingHorizontal)
            {
                //左线的处理
                float detX=leftLine.getLeft()+moveX;
                if (detX>=rightLine.getLeft()-padding)
                    detX=rightLine.getLeft()-padding;
                if(detX<=0)
                    detX=0;
                leftLine.setLeft(detX);
                leftLine.setRight(detX);
            }
            if(!isMovingLeftLine&&isMovingHorizontal)
            {
                //右线的处理
                float detX=rightLine.getLeft()+moveX;
                if(detX<=leftLine.getLeft()+padding)//和上线保持20的像素点
                    detX=leftLine.getLeft()+padding;
                if(detX>=startWidth)//不可超过边线
                    detX=startWidth;
                rightLine.setLeft(detX);
                rightLine.setRight(detX);
            }
            if(isMovingVertical||isMovingHorizontal)
               invalidate();
            Log.i("moveY",moveY+"");
            Log.i("isMovingVertical",isMovingVertical+"");
            Log.i("isMovingHorizontal",isMovingHorizontal+"");
            Log.i("上线",isMovingUpLine+"");
            Log.i("moveX",moveX+"");
            Log.i("左线",isMovingLeftLine+"");
            return true;
        }

        if(event.getAction()==MotionEvent.ACTION_UP)
        {

            return true;
        }
        return false;
    }

    /**
     * 获取截图的范围
     * @return
     */
    public Line getClipLine()
    {
        Bitmap bitmap=((ClipActivity)context).getBitmap();
        bitmapWidth=bitmap.getWidth();
        bitmapHeight=bitmap.getHeight();

        //计算当前显示的图片和原始图片的缩放比例
        float xRatio=bitmapWidth/(startWidth-getPaddingRight()-getPaddingLeft());
        float yRatio=bitmapHeight/(startHeight-getPaddingTop()-getPaddingBottom());
        //判断截图的XY坐标,如果小于0,则认为截取的图片XY是从0开始
        float clipX=(leftLine.getLeft()-getPaddingLeft())<=0?0:leftLine.getLeft()-getPaddingLeft();
        float clipY=(upLine.getTop()-getPaddingTop())<=0?0:upLine.getTop()-getPaddingTop();
        //获得截取的图片XY转换成原始图片的XY坐标
        float x=clipX*xRatio;
        float y=clipY*yRatio;
        //获得截取图片的宽高
        float clipWidth=(rightLine.getRight()-(startWidth-getPaddingRight())>=0?startWidth-getPaddingRight()-getPaddingLeft():rightLine.getRight())-clipX;
        float clipHeight=(downLine.getBottom()-(startHeight-getPaddingBottom())>=0?startHeight-getPaddingBottom()-getPaddingTop():downLine.getBottom())-clipY;
        //准换成原始图片的宽高
        float width=clipWidth*xRatio;
        float height=clipHeight*yRatio;

        Line line=new Line(x,y,width,height);
        return line;
    }
}

效果图就不列出来了,这些源码是笔者自己项目需要写的demo,不方便全部展示,但是逻辑流程可以照搬。

---------文章写自:HyHarden---------

--------博客地址:http://blog.csdn.net/qq_25722767-----------

时间: 2024-12-14 17:16:19

自定义ImageView实现局部截图功能的相关文章

自定义Imageview控件实现多种手势操作 (拖动、水平缩放、竖直缩放、等比例缩放、双击、长按)

项目中需要使用自定义控件的多种手势操作,之前在网上查阅资料的时候发现能找到的一般是只实现了其中的几种,这次就把我做的控件分享一下,人人为我,我为人人嘛,哈哈! 这个自定义控件实现的主要功能是控件的拖动和缩放(注意:不是对控件中的图片进行操作,话说很多帖子都把这两个混了),其中缩放可以按照三个方向进行,就是水平.竖直和等比例.双击操作只做了一个提示,长按加上了一个简单的弹出菜单. 抱歉的是没有足够的时间写详细注释了,如果跟你需要的功能相同就请直接调用,要是需要改代码就费点神自己读懂代码吧,看不懂的

C#软件开发实例.私人订制自己的屏幕截图工具(九)使用自定义光标,QQ截图时的光标

在使用QQ的截图功能的时候,是不是觉得它的光标很酷呢?今天就说一下怎么应用自定义光标,在我们的截图工具中使用QQ截图的光标. 打开资源: 切换到文件资源视图: 打开资源文件目录,将光标文件复制到此目录下: 所需光标文件下载:C#软件开发实例.私人订制自己的屏幕截图工具中使用的光标文件 选中Resources目录,刷新,显示出刚刚复制进来的光标文件: 选中光标文件,拖动到资源的文件视图中: 资源资源名称中的单词第一个字母改为大写. 光标预览: 在Form1类中添加私有变量: #region 自定义

Unity3D截图功能总结,并保存在指定的文件夹

快过新年了,一直在加班赶项目,没时间写博客,今天上班最后一天,就休息过年了,将我强几天在做一个截图功能分享出来,网上查了很多,但是都是在Unity Editor下好使,能截图,并显示出来,但是,在Android下,截图成功,并能显示出来,但是就是存不到手机相册中,找了很多原因不知道怎么回事,查阅各种资料最终解决了.我总结了一下,我用过的方法,希望大家 能够用的上. 第一种方法: 使用Application类下的CaptureScreenshot方法.但是我觉得这并不好用,不随意.不能针对某一个相

Unity3d的截图功能

下面是我总结的.在u3d中的,三种截屏方法: 1.使用Application类下的CaptureScreenshot方法. [csharp] view plaincopy void CaptureScreen() { Application.CaptureScreenshot("Screenshot.png", 0); } 这个方法,截取的是某一帧时整个游戏的画面,或者说是全屏截图吧. a.不能针对某一个相机(camera)的画面,进行截图. b.对局部画面截图,实现起来不方便,效率也

封装selenium自动化框架中的截图功能

对selenium自带的截图功能进行封装: 以下为封装的代码,自定义一个.py文件即可,图片路径自己设置一个. 1 #coding:utf-8 2 3 class Screen(object): 4 ''' 5 封装的截图类,webdriver自带的get_screenshot_as_file() 6 在使用过程中,注意driver参数的传递 7 ''' 8 def __init__(self, driver): 9 ''' 10 写一个构造函数,有一个参数driver 11 ''' 12 se

canvas与html5实现视频截图功能

这段时间一直在研究canvas,突发奇想想做一个可以截屏视频的功能,然后把图片拉去做表情包,哈哈哈哈哈哈~~ 制作方法: 1.在页面中加载视频 在使用canvas制作这个截图功能时,首先必须保证页面上已经加载完成了这个视频,这样才能够方便的对其操作.如果使用下面直接嵌入<video>标签的方式: 1 2 3 4 5 <video loop controls id="testmp4" width="500" height="400"

安卓开发_实现截图功能

4-17号在360云盘提供了 群雄兵法APP的下载,将其作为测试版,在短短10天内便下载量超过1000,虽说不是很高,但是也证明我一个月的努力是值得的. 这两天游戏更新了,我也将推出最新的版本,在原来的测试版本的基础上,修改了一些内容的错误,优化了部分界面,添加了武将生平介绍,武将成长值等资料. 因为广大的用户提出装备模拟之后无法保存的问题,我在自己所学的基础上进行的解决,但是无奈没有解决成功,于是乎先用截图功能来实现装备模拟后的属性保存吧. 网上百度了很多,大部分相当复杂,对于我这个水平来说,

截图功能的简单实现

//截图功能的实现 //1.建立一个bitmapContext UIGraphicsBeginImageContext(self.view.bounds.size); //2.获取bitmapContext CGContextRef context = UIGraphicsGetCurrentContext(); //3.把self.view.layer图层的全部内容渲染到bitmapContext上去. //截取当前图层的一部分 UIRectClip(CGRectMake(100, 100,

C#软件开发实例.个人定制自己的屏幕抓图工具(八)加入了截图功能键盘

章文件夹 (一)功能概览 (二)创建项目.注冊热键.显示截图主窗体 (三)托盘图标及菜单的实现 (四)基本截图功能实现 (五)针对拖拽时闪烁卡顿现象的优化 (六)加入配置管理功能 (七)加入放大镜的功能 (八)加入键盘操作截图的功能 (九)使用自己定义光标,QQ截图时的光标 (十)在截图中包括鼠标指针形状 尽管加入了放大镜的功能,可是在进行像素级的定位时,还是不easy精确定位,在用鼠标操作时要改变一两个像素的位置还是有些困难的. 处理键盘按下事件 /// <summary> /// 处理键盘