Android开发技巧——定制仿微信图片裁剪控件
拍照——裁剪,或者是选择图片——裁剪,是我们设置头像或上传图片时经常需要的一组操作。上篇讲了Camera的使用,这篇讲一下我对图片裁剪的实现。
背景
下面的需求都来自产品。 裁剪图片要像微信那样,拖动和放大的是图片,裁剪框不动。 裁剪框外的内容要有半透明黑色遮罩。 裁剪框下面要显示一行提示文字(这点我至今还是持保留意见的)。在Android中,裁剪图片的控件库还是挺多的,特别是github上比较流行的几个,都已经进化到比较稳定的阶段,但比较遗憾的是它们的裁剪过程是拖动或缩放裁剪框,于是只好自己再找,看有没有现成或半成品的轮子,可以不必从零开始。
大神的实现过程
首先先了解一下上面的高仿微信裁剪控件的实现过程。说起来也不难,主要是下面几点:
1,重写ImageView
,并监听手势事件,包括双点,两点缩放,拖动,使它成为一个实现缩放拖动图片功能的控件。
2,定义一个Matrix
成员变量,对于维护该图片的缩放、平移等矩阵数据。
3,拖动或缩放时,图片与裁剪框的相交面积一定与裁剪框相等。即图片不能拖离裁剪框。
3,在设置图片时,先根据图片的大小进行初始化的缩放平移操作,使得上面第三条的条件下图片尽可能的小。
4,每次接收到相对应的手势事件,都进行对应的矩阵计算,并将计算结果通过ImageView
的setImageMatrix
方法应用到图片上。
5,裁剪框是一个单独的控件,与ImageView
同样大,叠加到它上面显示出来。
6,用一个XXXLayout
把裁剪框和缩放封装起来。
7,裁剪时,先创建一个空的Bitmap并用其创建一个Canvas
,把缩放平移后的图片画到这个Bitmap上,并创建在裁剪框内的Bitmap
(通过调用Bitmap.createBitmap
方法)。
我的定制内容
我拿到的代码是鸿洋大神版本之后再被改动的,代码上有点乱(虽然功能上是实现的裁剪)。在原有的功能上,我希望进行的改动有:
合并裁剪框的内容到ImageView中 裁剪框可以是任意长宽比的矩形 裁剪框的左右外边距可以设置 遮罩层颜色可以设置 裁剪框下有提示文字(自己的产品需求) 后面产品又加入了一条裁剪图片的最大大小属性定义
在上面的功能需求中,我定义了以下属性:
<code class="language-xml hljs "><declare-styleable name="ClipImageView"> <attr name="civHeight" format="integer"> <attr name="civWidth" format="integer"> <attr name="civTipText" format="string"> <attr name="civTipTextSize" format="dimension"> <attr name="civMaskColor" format="color"> <attr name="civClipPadding" format="dimension"> </attr></attr></attr></attr></attr></attr></declare-styleable></code>
其中:
civHeight
和civWidth
是裁剪框的宽高比例。 civTipText
提示文字的内容 civTipTextSize
提示文字的大小 civMaskColor
遮罩层的颜色值 civClipPadding
裁剪内边距。由于裁剪框是在控件内部的,最终我选择使用padding来说明裁剪框与我们控件边缘的距离。
成员变量
成员变量我进行了一些改动,把原本用于定义裁剪框的水平边距变量及其他没什么用的变量等给去掉了,并加入了自己的一些成员变量,最终如下:
private final int mMaskColor;//遮罩层颜色
private final Paint mPaint;//画笔
private final int mWidth;//裁剪框宽的大小(从属性上读到的整型值)
private final int mHeight;//裁剪框高的大小(同上)
private final String mTipText;//提示文字
private final int mClipPadding;//裁剪框相对于控件的内边距
private float mScaleMax = 4.0f;//图片最大缩放大小
private float mScaleMin = 2.0f;//图片最小缩放大小
/**
* 初始化时的缩放比例
*/
private float mInitScale = 1.0f;
/**
* 用于存放矩阵
*/
private final float[] mMatrixValues = new float[9];
/**
* 缩放的手势检查
*/
private ScaleGestureDetector mScaleGestureDetector = null;
private final Matrix mScaleMatrix = new Matrix();
/**
* 用于双击
*/
private GestureDetector mGestureDetector;
private boolean isAutoScale;
private float mLastX;
private float mLastY;
private boolean isCanDrag;
private int lastPointerCount;
private Rect mClipBorder = new Rect();//裁剪框
private int mMaxOutputWidth = 0;//裁剪后的图片的最大输出宽度
构造方法
构造方法里主要是多了一些我们自定义属性的读取:
public ClipImageView(Context context) {
this(context, null);
}
public ClipImageView(Context context, AttributeSet attrs) {
super(context, attrs);
setScaleType(ScaleType.MATRIX);
mGestureDetector = new GestureDetector(context,
new SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (isAutoScale)
return true;
float x = e.getX();
float y = e.getY();
if (getScale() < mScaleMin) {
ClipImageView.this.postDelayed(new AutoScaleRunnable(mScaleMin, x, y), 16);
} else {
ClipImageView.this.postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16);
}
isAutoScale = true;
return true;
}
});
mScaleGestureDetector = new ScaleGestureDetector(context, this);
this.setOnTouchListener(this);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.WHITE);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClipImageView);
mWidth = ta.getInteger(R.styleable.ClipImageView_civWidth, 1);
mHeight = ta.getInteger(R.styleable.ClipImageView_civHeight, 1);
mClipPadding = ta.getDimensionPixelSize(R.styleable.ClipImageView_civClipPadding, 0);
mTipText = ta.getString(R.styleable.ClipImageView_civTipText);
mMaskColor = ta.getColor(R.styleable.ClipImageView_civMaskColor, 0xB2000000);
final int textSize = ta.getDimensionPixelSize(R.styleable.ClipImageView_civTipTextSize, 24);
mPaint.setTextSize(textSize);
ta.recycle();
mPaint.setDither(true);
}
定义裁剪框
裁剪框的位置
裁剪框是在控件正中间的,首先我们从属性中读取到的是宽高的比例,以及左右边距,但是在构造方法中,由于控件还没有绘制出来,无法获取到控件的宽高,所以并不能计算裁剪框的大小和位置。所以我重写了onLayout方法,在这里计算裁剪框的位置:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
final int width = getWidth();
final int height = getHeight();
mClipBorder.left = mClipPadding;
mClipBorder.right = width - mClipPadding;
final int borderHeight = mClipBorder.width() * mHeight / mWidth;
mClipBorder.top = (height - borderHeight) / 2;
mClipBorder.bottom = mClipBorder.top + borderHeight;
}
绘制裁剪框
这里我顺便把绘制提示文字的代码也一并给出,都是在同一个方法里的。很简单,重写onDraw
方法即可。绘制裁剪框有两种方法,一是绘制一个满屏的遮罩层,然后从中间抠出一个长方形出来,但是我用的时候发现抠不出来,所以我采用的是下面这一种:
先画上下两个矩形,再画左右两个矩形,中间所围起来的没有画的部分就是我们的裁剪框。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);