自定义View之App用户头像截取控件

1、引言

在平时开发App过程中,用户需要设置头像的,从众多app的头像设置来看,基本可分为2中情况:1)圆形头像(如QQ,新浪微博等);2)矩形头像(如微信等).
其实也有好多高仿QQ截图或者微信头像截图的博文,但是都不是真正的高仿,只能作为一个参考,直接拿来用还不够完美,索性自己的也需要这个控件,自己动手写一个.

2、功能分析

效果图

该控件可通过属性设置头像的大小即中间截图区域的宽度或者半径(代码中通过设置左右边界padding来调节截图窗口),也可以通过手势缩放图片来进行选择你想要的区域,也支持双击图片可自动缩放大小,并且可以根据手指的移动图片的显示.
由上述功能可分析出,该控件像是有2个控件叠加的产生的效果,一个ImageView控件,一个中间镂空的透明层控件.
对于矩形截图框:我们可以按照以下结构图进行填充:将整个画布分为5部分,其中1-4部分利用设置画笔的透明度来进行填充.
结构图
对于圆形截图框:在Android在中绘制机制中有一个很神奇的类PorterDuffXferMode,如图所示,它可以利用两个画布的不同叠加模式得到不同的形状,上述控件的效果就可以使用这个类来实现.
PorterDuffXferMode

3、RectOrCircleImageView实现

因为本文设计的控件可以通过用户的设置进行选择需要哪一种头像截图(圆形还是矩形),代码中对用RECT和CIRCLE,并提供外部可访问的方法对mType进行设置.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 控件类型选择
*/
public enum TYPE {RECT, CIRCLE}
/**
* 头像截图类型,默认是矩形
*/
private TYPE mType = TYPE.RECT;
/**
* 设置绘制的形状
*/
public TYPE getmType() {
return mType;
}
public void setmType(TYPE mType) {
this.mType = mType;
}

RectOrCircleImageView的具体实现分为三步,第一步先实现矩形头像截图;第三部实现圆形头像截图并添加一些手势操作;第三步实现图像截取以及双击图像变大变小.

3.1 矩形头像截图设计

实现矩形头像截图需要做的事情总的来说就是:在一张图片上镂空一个矩形区域,其他的区域“隐藏”。如结构图所示,我们将1-4区域用具有一定透明度的画笔进行绘制,第五部分的区域不管它就可以完成,具体步骤在代码中讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* 水平方向与View的边距,可通过属性进行设置
*/
private int mHorizontalPadding = 30;
/**
* 垂直方向与View的边距
*/
private int mVerticalPadding;
/**
* 圆的直径或者矩形宽度
*/
private int mWidthOrRadius = 30;
/**
* 默认的边界颜色
*/
private int mBorderColor = Color.parseColor("#FFFFFF");
/**
* 边界的宽度
*/
private int mBorderWid = 1;
/**
* 画笔
*/
private Paint mRectPaint;
public RectOrCircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
super.setScaleType(ScaleType.MATRIX);//缩放模式为矩阵,之后会用到
this.setBackgroundColor(Color.BLACK);
this.mBorderWid = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,mBorderWid,
getResources().getDisplayMetrics());
this.mWidthOrRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,mWidthOrRadius,
getResources().getDisplayMetrics());
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.RectOrCircleImageView);
this.mHorizontalPadding = (int) a.getDimension(R.styleable.RectOrCircleImageView_width,30)+ 5;
a.recycle();
/* Log.d("Width","->" + mHorizontalPadding);*/
this.mRectPaint = new Paint();
mRectPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mType == TYPE.RECT)
{
//计算矩形截图区域宽度
mWidthOrRadius = getWidth() - 2 * mHorizontalPadding;
//计算垂直方向的间距
mVerticalPadding = (getHeight() - mWidthOrRadius) / 2;
//设置画笔的透明度以及颜色
mRectPaint.setColor(Color.parseColor("#50000000"));
mRectPaint.setStyle(Paint.Style.FILL);
//绘制左边矩形
canvas.drawRect(0,0,mHorizontalPadding,getHeight(),mRectPaint);
//绘制右边矩形
canvas.drawRect(getWidth() - mHorizontalPadding,0,getWidth(),getHeight(),mRectPaint);
//绘制上边矩形
canvas.drawRect(mHorizontalPadding,0,getWidth() - mHorizontalPadding,mVerticalPadding,mRectPaint);
//绘制下边矩形
canvas.drawRect(mHorizontalPadding,getHeight() - mVerticalPadding,getWidth() - mHorizontalPadding,getHeight(),mRectPaint);
// 绘制截图边框
mRectPaint.setColor(mBorderColor);
mRectPaint.setStrokeWidth(mBorderWid);
mRectPaint.setStyle(Paint.Style.STROKE);
canvas.drawRect(mHorizontalPadding,mVerticalPadding,getWidth() - mHorizontalPadding,
getHeight() - mVerticalPadding,mRectPaint);
}else if(mType == TYPE.CIRCLE){
//代码省略,后面讲
}
}

上述代码中我们通过设置水平方向的边距mHorizontalPadding(默认是30,可通过属性进行设置),根据边距计算出正方形的边长,接下来就是按照结构图分别绘制1、2、3、4四个区域,最后就是绘制我们的正方形区域的边界(其实就是绘制一个空心的矩形).

3.2 圆形头像截图设计

圆形的截图区域就不能直接用绘制的方式,因为系统没有提供这个格式的画笔啊,所以我们采用PorterDuffXferMode的Xor模式来进行镂空处理,具体就是在图片上(或者说是控件上)绘制一层遮罩层,这层有点像矩形截图区域中绘制的透明层一样,就是“掩盖”住原图,再在以控件的中心位置为圆点,绘制一个实心圆,这时设置一直画笔的PorterDuffXfermode模式为XOR,并再次在上面的实心圆上绘制同样大小的圆,这样就可以得到我们要的效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
else if(mType == TYPE.CIRCLE){
/**
* 利用Xfermode进行设置:Xor挖空中间显示部分
*/
//计算圆形的半径
mWidthOrRadius = getWidth() - 2 * mHorizontalPadding;
//计算垂直方向的间距
mVerticalPadding = (getHeight() - mWidthOrRadius) / 2;
//遮罩层的画笔设置
mRectPaint.setColor(Color.parseColor("#50000000"));
mRectPaint.setStyle(Paint.Style.FILL);
//阴影层为整个屏幕大小(整个控件)
RectF rectF = new RectF(0,0,getWidth(),getHeight());
// 绘制不透明的实心圆画笔
Paint mPaintCirle = new Paint();
mPaintCirle.setStrokeWidth(mWidthOrRadius / 2);
mPaintCirle.setARGB(255,0,0,0);//黑色不透明
//圆形画笔设置
mCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
//阴影层图像
Bitmap mCircleBmp = Bitmap.createBitmap(getWidth(),getHeight(), Bitmap.Config.ARGB_8888);
Canvas mCanvas = new Canvas(mCircleBmp);
//绘制阴影层(整个屏幕)
mCanvas.drawRect(rectF,mRectPaint);
//绘制实心圆,绘制完后,在mCanvas画布中,mPaintRect和mPaintCirle相交部分即被掏空
mCanvas.drawCircle( getWidth()/2, getHeight()/2, mWidthOrRadius / 2, mPaintCirle);
mCanvas.drawCircle( getWidth()/2, getHeight()/2, mWidthOrRadius / 2, mCirclePaint);
//将扣空之后的阴影画布添加到原来的画布canvas中
canvas.drawBitmap(mCircleBmp, null, rectF, new Paint());
//绘制圆环
mRectPaint.setColor(mBorderColor);
mRectPaint.setStrokeWidth(mBorderWid);
mRectPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle( getWidth() / 2, getHeight() / 2, mWidthOrRadius / 2, mRectPaint);
}

在上述代码中我们已经完成了矩形和圆形的设计,但在平时选择头像图像时都会放大或者缩小图片选择喜欢的区域,所以下面会通过手势缩放图像并添加手势移动图像的功能.
但是在图片第一次加载到控件时,为了视觉上的完美,需要将原始图片本身大小大于屏幕大小的图片缩放到屏幕大小并将其移动至屏幕中心处,这个过程需要在该控件还没有加载到ViewTree视觉树之前进行处理,即在onGlobalLayout()方法中对原图进行缩放并根据mWidthOrRadius计算出最小的缩放比例,若图片本身大小都小于mWidthOrRadius那么就不需要处理,具体细节看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* View添加到Window,在视图树中添加GlobalLayout监听
*/
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
/**
* 对控件图片资源进行缩放,若大于屏幕长宽,则缩放至屏幕大小
* 1:长比屏幕长;
* 2:宽比屏幕框;
* 3:长宽都长
*/
@Override
public void onGlobalLayout() {//整个视图树布局发生变化时触发(该方法会被调用多次)
if(isOnce){
Drawable drawable = getDrawable();//获取图片资源
if(drawable == null)
return;
int viewWid = getWidth();//控件宽
int viewHei = getHeight();//控件高
//图片宽高
int imgWid = drawable.getIntrinsicWidth();
int imgHei = drawable.getIntrinsicHeight();
float scale = 1.0f;
//图片宽度大小屏幕(控件),高度小于等于屏幕,则缩放至屏幕的宽或者高
if(imgWid > viewWid && imgHei <= viewHei){
scale = viewWid * 1.0f / imgWid;
}
if(imgHei > viewHei && imgWid <= viewWid){//图片高度大于屏幕
scale = viewHei * 1.0f / imgHei;
}
// 如果宽和高都大于屏幕,则按比例最小适应屏幕大小
if (imgWid > viewWid && imgHei > viewHei)
{
scale = Math.min(viewHei * 1.0f / imgHei, viewWid * 1.0f / imgWid);
}
mScale = scale;
/**
* 计算以中间矩形或者圆形的外接矩形最小区域变成为最小边界并计算缩放比例
*/
//计算最小的缩放比例
SCALE_MIN = Math.max(mWidthOrRadius * 1.0f/ imgWid,mWidthOrRadius * 1.0f / imgHei) > 0.0f ?
Math.max(mWidthOrRadius * 1.0f/ imgWid,mWidthOrRadius * 1.0f/ imgHei) : 1.0f;
Log.e("SCALE",SCALE_MIN + ", " + SCALE_MAX + ", " + SCALE_MID);
//将图片缩放并移动至屏幕中心
mScaleMatrix.postTranslate((viewWid - imgWid) / 2, (viewHei - imgHei) / 2);
mScaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
setImageMatrix(mScaleMatrix);//重绘ImageView显示
isOnce = false;
}
}

上面函数的功能就是完成图片的缩放并移动到屏幕中心.
下面实现手势缩放,控件需要实现接口ScaleGestureDetector.OnScaleGestureListener,在其方法onScale中实现手势缩放功能.但是在缩放过程中如果没有边界判断,就会出现白色的空隙,如果移动到截图区域,给用户不好的体验,所以我们要以中心的截图区域为边界,若缩放之后的图片位置已经在截图框内,则会自动移动到截图区域边界处.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* 最大、中等缩放比例
*/
private static final float SCALE_MAX = 4.0f;
private static final float SCALE_MID = 2.0f;
private float SCALE_MIN;//最小缩放比例需要在控件显示后计算
/**
* 图片缩放比例:默认大小不变
*/
private float mScale = 1.0f;
/**
* 缩放的手势检测
*/
private ScaleGestureDetector mScaleGestureDetector;
/**
* 根据手势的变化,缩放图片
* @param detector
* @return
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
//获得当前的缩放比例
float scale = getScale();
float scaleFactor = detector.getScaleFactor();//获得手势缩放因子(将要缩放的比例)
if(getDrawable()==null)
return true;//不是一个缩放手势
/**
* 缩放范围
* 1:放大比例
* 2:缩小比例
*/
if((scale < SCALE_MAX && scaleFactor > 1.0f) || (scale >= mScale && scaleFactor < 1.0f)){
//缩小
if((scaleFactor * scale) < SCALE_MIN){
scaleFactor = SCALE_MIN / scale;
}
//当前手势要放大的比例大于最大比例,则最多扩大得到最大比例
if(scaleFactor * scale > SCALE_MAX){
scaleFactor = SCALE_MAX / scale;
}
//以图片中心为缩放点
//mScaleMatrix.postScale(scaleFactor,scaleFactor,getWidth()/2,getHeight()/2);
//以手势点击处为中心点进行缩放
mScaleMatrix.postScale(scaleFactor,scaleFactor,detector.getFocusX(),detector.getFocusY());
//边界判断
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
}
return true;
}
/**
* 在手势进行缩放时,需要进行边界检测,移动图片至屏幕中间,避免缩放出现白边
*/
private void checkBorderAndCenterWhenScale(){
RectF rectF = getMatrixRectF();
//X、Y方向偏移量
float deltaX = 0;
float deltaY = 0;
int wid = getWidth();
int hei = getHeight();
//当前图片宽大于屏幕,则控制范围
if(rectF.width() > wid){
if(rectF.left > mHorizontalPadding){//左边出现了空白区域
// deltaX = -rectF.left;
deltaX = -(rectF.left - mHorizontalPadding);//只是回到框的边界处
}
if(rectF.right < wid - mHorizontalPadding){//右边出现空白区域
//deltaX = wid - rectF.right;
deltaX = wid - rectF.right - mHorizontalPadding;
}
}
//当前图片高大于屏幕,则控制范围
if(rectF.height() > hei){
if(rectF.top > mVerticalPadding){
deltaY = -(rectF.top - mVerticalPadding);
}
if(rectF.bottom < hei - mVerticalPadding){
deltaY = hei - rectF.bottom - mVerticalPadding;
}
}
//若图片宽高小于屏幕,则让其居中
if(rectF.width() < wid){
deltaX = wid * 0.5f - rectF.right + 0.5f * rectF.width();
}
if(rectF.height() < hei){
deltaY = hei * 0.5f - rectF.bottom + 0.5f * rectF.height();
}
//平移(应该加一个动画延迟)
mScaleMatrix.postTranslate(deltaX,deltaY);
}
/**
* 根据当前的缩放矩阵计算图片当前的位置范围
* @return
*/
private RectF getMatrixRectF(){
Matrix m = mScaleMatrix;
RectF rectF = new RectF();
Drawable d = getDrawable();
if(d!=null){
//整张图的四个顶点
rectF.set(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
//根据矩阵变换图片的四个顶点位置
m.mapRect(rectF);
}
return rectF;
}

现在完成了图像的放大缩小,那么在放大之后我们想要移动图片进行区域选择,所以需要添加一个手指移动的公共.
控件实现接口View.OnTouchListener监听,并实现onTouch方法.在移动图像是也需要进行边界的判断,也不允许出现空白区域.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* 移动滑动距离阈值
*/
private float mTouchSlop;
/**
* 触点位置坐标
*/
private float mLastX;
private float mLastY;
private int lastPointCount = 0;//上一次触点的个数
/**
* 在移动时需要判断当前图片宽高是否小于屏幕大小,需不需要进行调整移动
*/
private boolean isCheckLeftAndRight = false;
private boolean isCheckTopAndBottom = false;
/**
* 移动图片
* @param v
* @param event
* @return
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
mScaleGestureDetector.onTouchEvent(event);//手势接受Touch事件
float x = 0;
float y = 0;
//触点个数并计算它们的均值
final int pointCount = event.getPointerCount();
for(int i = 0;i < pointCount;i++){
x += event.getX(i);
y += event.getY(i);
}
x = x / pointCount;
y = y / pointCount;
//还不能拖动之前一直更新位置
if(pointCount != lastPointCount){
isCanDrag = false;
mLastX = x;
mLastY = y;
}
lastPointCount = pointCount;
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
float dx = x - mLastX;//
float dy = y - mLastY;
if(!isCanDrag){
isCanDrag = isCanDrag(dx,dy);
}
if(isCanDrag) {
//可以拖动
RectF rectF = getMatrixRectF();//当前图片位置
if(getDrawable()!=null){
isCheckLeftAndRight = isCheckTopAndBottom = true;
//若图片宽度小于截图区域宽度,则禁止左右移动
if(rectF.width() < mWidthOrRadius){
dx = 0;
isCheckLeftAndRight = false;
}
if(rectF.height() < mWidthOrRadius){
dy = 0;
isCheckTopAndBottom = false;
}
mScaleMatrix.postTranslate(dx,dy);//移动
//检测移动是否小于屏幕,不让出现白边
checkMoveBounds();
setImageMatrix(mScaleMatrix);
}
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
lastPointCount = 0;
break;
}
return true;
}
/**
* 移动时,进行边界判断
*/
private void checkMoveBounds(){
//当前图片的位置
RectF rectF = getMatrixRectF();
float deltaX = 0, deltaY = 0;
final float viewWidth = getWidth();
final float viewHeight = getHeight();
// 判断移动或缩放后,图片显示是否超出截图边界
if(rectF.top > mVerticalPadding && isCheckTopAndBottom){
deltaY = - (rectF.top - mVerticalPadding);//需要偏移的距离
}
if(rectF.bottom < viewHeight - mVerticalPadding && isCheckTopAndBottom){
deltaY = viewHeight - (rectF.bottom + mVerticalPadding);
}
if(rectF.left > mHorizontalPadding && isCheckLeftAndRight){
deltaX = -(rectF.left - mHorizontalPadding);
}
if (rectF.right < viewWidth - mHorizontalPadding && isCheckLeftAndRight)
{
deltaX = viewWidth - (rectF.right + mHorizontalPadding);
}
mScaleMatrix.postTranslate(deltaX,deltaY);//移动图像
}
/**
* 判断当前移动的距离是否满足阈值
* @param dx
* @param dy
* @return
*/
private boolean isCanDrag(float dx,float dy){
return Math.sqrt((dx * dx) + (dy * dy)) <= mTouchSlop;
}

通过上述的代码可以进行图像的放大缩小,并移动图像选取用户感兴趣的区域.

3.3 双击图片变大并实现图像截取

在平时使用时,也会出现需要图片一瞬间变大变小的功能,在这里本控件也添加以双击图片变大的功能,放大范围分为3种:1)当前缩放比例在2以下的,双击后缩放至2倍大小;2)当前缩放比例大于2,双击后缩放至4倍大小;3)当前倍数已经为4时,则缩小到最初原始的大小并移动至屏幕的中心.
需要捕获控件的双击事件,则需要使用GestureDetector设置监听事件,我们这里只使用onDoubleTap这个回调方法,所以使用内部类SimpleOnGestureListener.
还有一个问题,如果在双击控件之后图片突然放大到某一倍数,会给用户一种不好的体验,所以在这里需要使用一个渐变缩放的过程,使用postDelayed执行一个Runnable线程,在线程中再根据当前的缩放值进行渐进缩放.
我们在构造函数中完成对GestureDetector的初始化,具体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public RectOrCircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
super.setScaleType(ScaleType.MATRIX);//缩放模式为矩阵,之后会用到
mScaleGestureDetector = new ScaleGestureDetector(context,this);//手势
this.setBackgroundColor(Color.BLACK);
/**
* 给手势添加双击监听事件
*/
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
//正在自动缩放,则不处理这次双击
if(isAutoScale)
return true;
//事件点击位置
float x = e.getX();
float y = e.getY();
//小于2.0f,放大至2倍
//Log.e("DoubleTap", getScale() + " , " + mScale);
if(getScale() < SCALE_MID){
RectOrCircleImageView.this.postDelayed(new AutoScaleThread(x,y,SCALE_MID),16);
isAutoScale = true;
}
//2.0f~4.0f
else if(getScale() >= SCALE_MID && getScale() < SCALE_MAX){
RectOrCircleImageView.this.postDelayed(new AutoScaleThread(x,y,SCALE_MAX),16);
isAutoScale = true;
}else{
RectOrCircleImageView.this.postDelayed(new AutoScaleThread(x,y,mScale),16);
isAutoScale = true;
}
return true;
}
});
//省略代码
}
/**
* 缩放延迟线程(动画过度)
*/
private class AutoScaleThread implements Runnable{
static final float BIGGER = 1.07f;//每次放大的一个比例
static final float SMALLER = 0.93f;//缩小
/**
* 缩放的位置坐标
*/
private float x;
private float y;
private float mTargetScale;
private float tmpScale;
public AutoScaleThread(float x,float y,float scale){
this.x = x;
this.y = y;
this.mTargetScale = scale;
//当前应该缩小还是放大
if(getScale() < mTargetScale){//当前值小于mTargetScale
tmpScale = BIGGER;
}else{
tmpScale = SMALLER;
}
}
/**
* 传入目标缩放值,根据目标值与当前值,判断应该放大还是缩小
*/
@Override
public void run() {
//每次都是以tmpScale与目标缩放值之间的比例进行缩放,每次放大一点
mScaleMatrix.postScale(tmpScale,tmpScale,x,y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
//缩放之后的scale是否合法
final float currentScale = getScale();
Log.d("AutoTrhead","currentScale:" + currentScale);
if( ((currentScale < mTargetScale) && (tmpScale >1.0f)) ||
((currentScale > mTargetScale) && ( tmpScale < 1.0f))){
RectOrCircleImageView.this.postDelayed(this,16);
}else{
//这时比例已经超出目标scale了,需要还原
final float deltaScale = mTargetScale / currentScale;
mScaleMatrix.postScale(deltaScale,deltaScale,x,y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
isAutoScale = false;
}
}
}

具体的步骤在注释中已经详细写出,通过上面的代码,可以完成图片的双击放大缩小.到了基本的功能已经完成,还有一个给外部的截图方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 按照当前的位置剪切头像
* @return
*/
public Bitmap clipBitmap(){
Bitmap bitmap = Bitmap.createBitmap(getWidth(),getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
//将屏幕上的图像绘制到当前canvas上
draw(canvas);
if(getmType() == TYPE.RECT){
//按照中间截图去的大小创建图像
return Bitmap.createBitmap(bitmap,mHorizontalPadding,mVerticalPadding,
getWidth() - 2 * mHorizontalPadding,getHeight() - 2 * mVerticalPadding);
}else if(getmType() == TYPE.CIRCLE){
return getCircleBitmap(Bitmap.createBitmap(bitmap,mHorizontalPadding,mVerticalPadding,
getWidth() - 2 * mHorizontalPadding,getHeight() - 2 * mVerticalPadding));
}
return null;
}
/**
* 头像为圆形时,在矩形区域的基础之上剪切
* @return
*/
private Bitmap getCircleBitmap(Bitmap bitmap){
Bitmap outBmp = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(outBmp);
final int color = 0xff424242;
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
Paint paint = new Paint();
paint.setAntiAlias(true);
canvas.drawARGB(0,0,0,0);
paint.setColor(color);
int x = bitmap.getWidth();
canvas.drawCircle(x / 2,x / 2,x / 2,paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap,rect,rect,paint);
return outBmp;
}

根据当前设置的截图区域选择不同的截图方法,但是要实现我们刚开始的效果图,还需要进一步的包装.
在该控件的上面还有一个文本“截图”,这个用于触发截图方法.我们这里继承RelativeLayout布局,将本文的控件和这个截图文本包装在一起,具体为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 包装RectOrCircleImageView
*/
public class RectOrCircleLayout extends RelativeLayout{
private RectOrCircleImageView mRectOrCircleImageView;
public RectOrCircleLayout(Context context) {
this(context,null);
}
public RectOrCircleLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public RectOrCircleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
RelativeLayout.LayoutParams relParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
//将控件添加到布局中
mRectOrCircleImageView = new RectOrCircleImageView(context);
this.addView(mRectOrCircleImageView,0,relParams);
//按钮
LinearLayout mLinear = new LinearLayout(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(5,5,5,5);
mLinear.setOrientation(LinearLayout.HORIZONTAL);
mLinear.setLayoutParams(params);
final TextView txtClip = new Button(context);
txtClip.setText("剪切");
txtClip.setBackgroundColor(Color.parseColor("#00000000"));
txtClip.setTextColor(Color.WHITE);
mLinear.setBackgroundColor(Color.parseColor("#60424348"));
mLinear.addView(txtClip);
//将“剪切”按钮添加到布局中
relParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
relParams.addRule(RelativeLayout.BELOW,mRectOrCircleImageView.getId());
this.addView(mLinear,1,relParams);
//实现接口,回调
txtClip.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onClipBitmapListener.onClipBitmap();
}
});
}
}

并且在RectOrCircleLayout中为了用户为RectOrCircleImageView添加图片、设置截图区域类型以及截图方法,添加了几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 为控件设置图片
* @param drawable
*/
public void setImgDrawable(Drawable drawable){
mRectOrCircleImageView.setImageDrawable(drawable);
}
public void setImgDrawable(Bitmap bitmap){
mRectOrCircleImageView.setImageBitmap(bitmap);
}
public void setImgDrawable(int resId){
mRectOrCircleImageView.setImageResource(resId);
}
public void setmType(RectOrCircleImageView.TYPE mType) {
mRectOrCircleImageView.setmType(mType);
}
//截图方法
public Bitmap clipBitmap(){
return mRectOrCircleImageView.clipBitmap();
}

需要得到截取到的图像,在控件中一个回调接口实现,并让“截图”按钮实现这个接口.

1
2
3
4
5
6
7
8
9
10
/**
* 回调接口
*/
OnClipBitmapListener onClipBitmapListener;
public void setOnClipBitmapListener(OnClipBitmapListener listener){
this.onClipBitmapListener = listener;
}
public interface OnClipBitmapListener{
void onClipBitmap();
}

4、使用实例

在布局文件中,添加控件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<com.lhqj.ClipImgView.RectOrCircleLayout
android:id="@+id/id_ClipView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
//实现接口OnClipBitmapListener
public class LayerListActivity extends Activity implements RectOrCircleLayout.OnClipBitmapListener{
private ImageView mImageView;
RectOrCircleLayout rectOrCircleLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.drawable_layout);
this.rectOrCircleLayout = (RectOrCircleLayout) findViewById(R.id.id_layout);
rectOrCircleLayout.setmType(RectOrCircleImageView.TYPE.RECT);
rectOrCircleLayout.setImgDrawable(BitmapFactory.decodeResource(getResources(),R.mipmap.gril2));
rectOrCircleLayout.setOnClipBitmapListener(this);
this.mImageView = (ImageView) findViewById(R.id.id_clipImgView);
}
//回调方法,得到截图
@Override
public void onClipBitmap() {
Bitmap m = rectOrCircleLayout.clipBitmap();
if(m!=null){
mImageView.setImageBitmap(m);
mImageView.setVisibility(View.VISIBLE);
rectOrCircleLayout.setVisibility(View.INVISIBLE);
}
}