1、前言
最近在做一个能够自选区域进行局部截图的功能,接下来,会给大家讲解,整个截图的实现过程。笔者这边实现的自选区域的形状是矩形,读者如果有需要,可以根据我给大家讲解的思路,修改成适合自己的截图工具。先来看看效果图
2、效果图
这里的图片是来自笔者对webView的截图产生的,读者可以根据自己的需要,替换上面的图片。
通过拖拽四条边框,可以实现屏幕的局部截图:
拖拽之后,只有需要截图的部分才会高亮显示,其余部分用遮罩掩盖。笔者实现的拖拽四条边都可以任意拖拽,并不一定要正方形或者长方形。也可以如下:
3、截图的基本知识
实现截图功能之前,我们需要了解View为我们提供的截图方法的使用。笔者会尽量详细讲述各个方法,让读者可以比较明白的去自由结合各截图方法的使用。
3.1、截图的基本知识点
在对View进行截图的时候,系统需要将当前View的内容缓存下来,因此,在截图之前,我们需要判断当前View是否允许视图缓存。
public boolean isDrawingCacheEnabled()
如果当前View允许进行视图缓存,会返回true否则返回false。如果返回false,我们就需要设置当前的视图,让它允许视图缓存。
public void setDrawingCacheEnabled(boolean enabled)
给当前视图强制生成缓存
public void buildDrawingCache(boolean autoScale)
此方法在视图不可缓存的时候,会强制生成一个视图缓存,autoScale表示是否自适应缩放缓存视图,如果为false的生成的视图大小和View的大小是一致的,true的话,则会根据内容生成自适应大小的图片。但是如果在调用此方法的之前,你没有调用setDrawingEnabled(true)的话,在对生成的视图使用完毕之后,应该对强制生成的视图缓存进行一个清理。清理的方法如下:
public void destroyDrawingCache()
此方法会将缓存的视图内容进行清理,从而对bitmap的资源进行释放回收利用。
获取缓存图像的方法:
public Bitmap getDrawingCache(boolean 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);//右
}
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,不方便全部展示,但是逻辑流程可以照搬。