Android如何自定义头像控件

在此输入图片描述

如上图效果: 效果分析

根据上面的效果,我们目测需要自定义两个控件,一个就是我们的可自由缩放移动的ImageView,一个就是那个白色的边框;然后一起放置到一个RelativeLayout中;最后对外公布一个裁剪的方法,返回一个Bitmap;

让我们来写代码吧~

首先是白色框框那个自定义View,我们叫做ClipImageBorderView

ClipImageBorderView

分析下这个View,其实就是根据在屏幕中绘制一个正方形,正方形区域以外为半透明,绘制这个正方形需要与屏幕左右边距有个边距。

我们准备按如下图绘制: 在此输入图片描述

代码:

[java] view plaincopy 01.package com.zhy.view;
02.
03.import android.content.Context;
04.import android.graphics.Canvas;
05.import android.graphics.Color;
06.import android.graphics.Paint;
07.import android.graphics.Paint.Style;
08.import android.util.AttributeSet;
09.import android.util.TypedValue;
10.import android.view.View;
11./** 12. * @author zhy 13. * 14. /
15.public class ClipImageBorderView extends View
16.{
17. /
* 18. * 水平方向与View的边距 19. /
20. private int mHorizontalPadding = 20;
21. /
* 22. * 垂直方向与View的边距 23. /
24. private int mVerticalPadding;
25. /
* 26. * 绘制的矩形的宽度 27. /
28. private int mWidth;
29. /
* 30. * 边框的颜色,默认为白色 31. /
32. private int mBorderColor = Color.parseColor("#FFFFFF");
33. /
* 34. * 边框的宽度 单位dp 35. */
36. private int mBorderWidth = 1;
37.
38. private Paint mPaint;
39.
40. public ClipImageBorderView(Context context)
41. {
42. this(context, null);
43. }
44.
45. public ClipImageBorderView(Context context, AttributeSet attrs)
46. {
47. this(context, attrs, 0);
48. }
49.
50. public ClipImageBorderView(Context context, AttributeSet attrs, int defStyle)
51. {
52. super(context, attrs, defStyle);
53. // 计算padding的px
54. mHorizontalPadding = (int) TypedValue.applyDimension(
55. TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources()
56. .getDisplayMetrics());
57. mBorderWidth = (int) TypedValue.applyDimension(
58. TypedValue.COMPLEX_UNIT_DIP, mBorderWidth, getResources()
59. .getDisplayMetrics());
60. mPaint = new Paint();
61. mPaint.setAntiAlias(true);
62. }
63.
64. @Override
65. protected void onDraw(Canvas canvas)
66. {
67. super.onDraw(canvas);
68. //计算矩形区域的宽度
69. mWidth = getWidth() - 2 * mHorizontalPadding;
70. //计算距离屏幕垂直边界 的边距
71. mVerticalPadding = (getHeight() - mWidth) / 2;
72. mPaint.setColor(Color.parseColor("#aa000000"));
73. mPaint.setStyle(Style.FILL);
74. // 绘制左边1
75. canvas.drawRect(0, 0, mHorizontalPadding, getHeight(), mPaint);
76. // 绘制右边2
77. canvas.drawRect(getWidth() - mHorizontalPadding, 0, getWidth(),
78. getHeight(), mPaint);
79. // 绘制上边3
80. canvas.drawRect(mHorizontalPadding, 0, getWidth() - mHorizontalPadding,
81. mVerticalPadding, mPaint);
82. // 绘制下边4
83. canvas.drawRect(mHorizontalPadding, getHeight() - mVerticalPadding,
84. getWidth() - mHorizontalPadding, getHeight(), mPaint);
85. // 绘制外边框
86. mPaint.setColor(mBorderColor);
87. mPaint.setStrokeWidth(mBorderWidth);
88. mPaint.setStyle(Style.STROKE);
89. canvas.drawRect(mHorizontalPadding, mVerticalPadding, getWidth()
90. - mHorizontalPadding, getHeight() - mVerticalPadding, mPaint);
91.
92. }
93.
94.}

我们直接预设了一个水平方向的边距,根据边距计算出正方形的边长,接下来就是按照上图分别会1、2、3、4四个区域,最后就是绘制我们的正方形~~

代码还是很简单的~~我们的ClipImageBorderView就搞定了,我们决定来测试一下:

布局文件:

[html] view plaincopy 01.<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
02. xmlns:tools="http://schemas.android.com/tools"
03. android:layout_width="match_parent"
04. android:layout_height="match_parent"
05. android:background="@drawable/a" >
06.
07. <com.zhy.view.ClipImageBorderView
08. android:id="@+id/id_clipImageLayout"
09. android:layout_width="fill_parent"
10. android:layout_height="fill_parent" />
11.
12.</RelativeLayout>

效果图:在此输入图片描述

这是图的效果。好看吧,good。 ClipZoomImageView

我们准备对我们原先的ZoomImageView进行简单的修改,修改的地方: 1、在onGlobalLayout方法中,如果图片的宽或者高只要一个小于我们的正方形的边长,我们会直接把较小的尺寸放大至正方形的边长;如果图片的宽和高都大于我们的正方形的边长,我们仅仅把图片移动到我们屏幕的中央,不做缩放处理;

2、根据步骤1,我们会获得初始的缩放比例(默认为1.0f),然后SCALE_MID , 与 SCALE_MAX 分别为2倍和4倍的初始化缩放比例。

3、图片在移动过程中的边界检测完全根据正方形的区域,图片不会在移动过程中与正方形区域产生内边距

4、对外公布一个裁切的方法

部分代码:

[java] view plaincopy 01./** 02. * 水平方向与View的边距 03. /
04. private int mHorizontalPadding = 20;
05. /
* 06. * 垂直方向与View的边距 07. /
08. private int mVerticalPadding;
09.
10. @Override
11. public void onGlobalLayout()
12. {
13. if (once)
14. {
15. Drawable d = getDrawable();
16. if (d == null)
17. return;
18. Log.e(TAG, d.getIntrinsicWidth() + " , " + d.getIntrinsicHeight());
19. // 计算padding的px
20. mHorizontalPadding = (int) TypedValue.applyDimension(
21. TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding,
22. getResources().getDisplayMetrics());
23. // 垂直方向的边距
24. mVerticalPadding = (getHeight() - (getWidth() - 2 * mHorizontalPadding)) / 2;
25.
26. int width = getWidth();
27. int height = getHeight();
28. // 拿到图片的宽和高
29. int dw = d.getIntrinsicWidth();
30. int dh = d.getIntrinsicHeight();
31. float scale = 1.0f;
32. if (dw < getWidth() - mHorizontalPadding * 2
33. && dh > getHeight() - mVerticalPadding * 2)
34. {
35. scale = (getWidth() * 1.0f - mHorizontalPadding * 2) / dw;
36. }
37.
38. if (dh < getHeight() - mVerticalPadding * 2
39. && dw > getWidth() - mHorizontalPadding * 2)
40. {
41. scale = (getHeight() * 1.0f - mVerticalPadding * 2) / dh;
42. }
43.
44. if (dw < getWidth() - mHorizontalPadding * 2
45. && dh < getHeight() - mVerticalPadding * 2)
46. {
47. float scaleW = (getWidth() * 1.0f - mHorizontalPadding * 2)
48. / dw;
49. float scaleH = (getHeight() * 1.0f - mVerticalPadding * 2) / dh;
50. scale = Math.max(scaleW, scaleH);
51. }
52.
53. initScale = scale;
54. SCALE_MID = initScale * 2;
55. SCALE_MAX = initScale * 4;
56. Log.e(TAG, "initScale = " + initScale);
57. mScaleMatrix.postTranslate((width - dw) / 2, (height - dh) / 2);
58. mScaleMatrix.postScale(scale, scale, getWidth() / 2,
59. getHeight() / 2);
60. // 图片移动至屏幕中心
61. setImageMatrix(mScaleMatrix);
62. once = false;
63. }
64.
65. }
66.
67. /
* 68. * 剪切图片,返回剪切后的bitmap对象 69. *
70. * @return 71. /
72. public Bitmap clip()
73. {
74. Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(),
75. Bitmap.Config.ARGB_8888);
76. Canvas canvas = new Canvas(bitmap);
77. draw(canvas);
78. return Bitmap.createBitmap(bitmap, mHorizontalPadding,
79. mVerticalPadding, getWidth() - 2 * mHorizontalPadding,
80. getWidth() - 2 * mHorizontalPadding);
81. }
82.
83. /
* 84. * 边界检测 85. */
86. private void checkBorder()
87. {
88.
89. RectF rect = getMatrixRectF();
90. float deltaX = 0;
91. float deltaY = 0;
92.
93. int width = getWidth();
94. int height = getHeight();
95.
96. // 如果宽或高大于屏幕,则控制范围
97. if (rect.width() >= width - 2 * mHorizontalPadding)
98. {
99. if (rect.left > mHorizontalPadding)
100. {
101. deltaX = -rect.left + mHorizontalPadding;
102. }
103. if (rect.right < width - mHorizontalPadding)
104. {
105. deltaX = width - mHorizontalPadding - rect.right;
106. }
107. }
108. if (rect.height() >= height - 2 * mVerticalPadding)
109. {
110. if (rect.top > mVerticalPadding)
111. {
112. deltaY = -rect.top + mVerticalPadding;
113. }
114. if (rect.bottom < height - mVerticalPadding)
115. {
116. deltaY = height - mVerticalPadding - rect.bottom;
117. }
118. }
119. mScaleMatrix.postTranslate(deltaX, deltaY);
120.
121. }

这里贴出了改变的代码,完整的代码就不贴了,太长了,如果大家学习过前面的博客应该也会比较熟悉,若没有也没事,后面会提供源码。

贴代码的目的,第一让大家看下我们改变了哪些;第二,我想暴露出我们代码中的问题,我们设置了一个这样的变量:mHorizontalPadding = 20;这个是手动和ClipImageBorderView里面的成员变量mHorizontalPadding 写的一致,也就是说这个变量,两个自定义的View都需要使用且需要相同的值,目前我们的做法,写死且每个View各自定义一个。这种做法不用说,肯定不好,即使抽取成自定义属性,两个View都需要进行抽取,且用户在使用的时候,还需要设置为一样的值,总觉得有点强人所难~~

5、不一样的自定义控件

现在我们考虑下:易用性。目前为止,其实我们的效果已经实现了,但是需要用户这么写布局文件:

[html] view plaincopy 01.<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
02. xmlns:tools="http://schemas.android.com/tools"
03. android:layout_width="match_parent"
04. android:layout_height="match_parent"
05. android:background="#aaaaaa" >
06.
07. <com.zhy.view.ZoomImageView
08. android:id="@+id/id_zoomImageView"
09. android:layout_width="fill_parent"
10. android:layout_height="fill_parent"
11. android:scaleType="matrix"
12. android:src="@drawable/a" />
13.
14. <com.zhy.view.ClipImageView
15. android:layout_width="fill_parent"
16. android:layout_height="fill_parent" />
17.
18.</RelativeLayout>

然后这两个类中都有一个mHorizontalPadding变量,且值一样,上面也说过,即使抽取成自定义变量,也需要在布局文件中每个View中各写一次。so, we need change . 这样的耦合度太夸张了,且使用起来蹩脚。

于是乎,我决定把这两个控件想办法整到一起,用户使用时只需要声明一个控件:

怎么做呢,我们使用组合的思想来自定义控件,我们再声明一个控件,继承子RelativeLayout,然后在这个自定义RelativeLayout中通过代码添加这两个自定义的布局,并且设置一些公用的属性,具体我们就开始行动。

、ClipImageLayout

我们自定义一个RelativeLayout叫做ClipImageLayout,用于放置我们的两个自定义View,并且由ClipImageLayout进行设置边距,然后传给它内部的两个View,这样的话,跟用户交互的就一个ClipImageLayout,用户只需要设置一次边距即可。

完整的ClipImageLayout代码:

[java] view plaincopy 01.package com.zhy.view;
02.
03.import android.content.Context;
04.import android.graphics.Bitmap;
05.import android.util.AttributeSet;
06.import android.util.TypedValue;
07.import android.widget.RelativeLayout;
08.
09.import com.zhy.clippic.R;
10./** 11. * zhy 12. * @author zhy 13. * 14. /
15.public class ClipImageLayout extends RelativeLayout
16.{
17.
18. private ClipZoomImageView mZoomImageView;
19. private ClipImageBorderView mClipImageView;
20.
21. /
* 22. * 这里测试,直接写死了大小,真正使用过程中,可以提取为自定义属性 23. /
24. private int mHorizontalPadding = 20;
25.
26. public ClipImageLayout(Context context, AttributeSet attrs)
27. {
28. super(context, attrs);
29.
30. mZoomImageView = new ClipZoomImageView(context);
31. mClipImageView = new ClipImageBorderView(context);
32.
33. android.view.ViewGroup.LayoutParams lp = new LayoutParams(
34. android.view.ViewGroup.LayoutParams.MATCH_PARENT,
35. android.view.ViewGroup.LayoutParams.MATCH_PARENT);
36.
37. /
* 38. * 这里测试,直接写死了图片,真正使用过程中,可以提取为自定义属性 39. /
40. mZoomImageView.setImageDrawable(getResources().getDrawable(
41. R.drawable.a));
42.
43. this.addView(mZoomImageView, lp);
44. this.addView(mClipImageView, lp);
45.
46.
47. // 计算padding的px
48. mHorizontalPadding = (int) TypedValue.applyDimension(
49. TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources()
50. .getDisplayMetrics());
51. mZoomImageView.setHorizontalPadding(mHorizontalPadding);
52. mClipImageView.setHorizontalPadding(mHorizontalPadding);
53. }
54.
55. /
* 56. * 对外公布设置边距的方法,单位为dp 57. *
58. * @param mHorizontalPadding 59. /
60. public void setHorizontalPadding(int mHorizontalPadding)
61. {
62. this.mHorizontalPadding = mHorizontalPadding;
63. }
64.
65. /
* 66. * 裁切图片 67. *
68. * @return 69. */
70. public Bitmap clip()
71. {
72. return mZoomImageView.clip();
73. }
74.
75.}

可以看到,现在用户需要使用头像裁切功能只需要声明下ClipImageLayout即可,完全避免了上述我们描述的问题,我们对用户屏蔽了两个真正实现的类。这个也是自定义控件的一种方式,希望可以借此抛砖引玉,大家能够更加合理的设计出自己的控件~~

好了,我们的ClipImageLayout搞定以后,下面看下如何使用~

6、用法

1、布局文件

[html] view plaincopy 01.<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
02. xmlns:tools="http://schemas.android.com/tools"
03. android:layout_width="match_parent"
04. android:layout_height="match_parent"
05. android:background="#aaaaaa" >
06.
07. <com.zhy.view.ClipImageLayout
08. android:id="@+id/id_clipImageLayout"
09. android:layout_width="fill_parent"
10. android:layout_height="fill_parent" />
11.
12.</RelativeLayout>

、MainActivity

[java] view plaincopy 01.package com.zhy.clippic;
02.
03.import java.io.ByteArrayOutputStream;
04.
05.import android.app.Activity;
06.import android.content.Intent;
07.import android.graphics.Bitmap;
08.import android.os.Bundle;
09.import android.view.Menu;
10.import android.view.MenuItem;
11.
12.import com.zhy.view.ClipImageLayout;
13.
14.public class MainActivity extends Activity
15.{
16. private ClipImageLayout mClipImageLayout;
17.
18. @Override
19. protected void onCreate(Bundle savedInstanceState)
20. {
21. super.onCreate(savedInstanceState);
22. setContentView(R.layout.activity_main);
23.
24. mClipImageLayout = (ClipImageLayout) findViewById(R.id.id_clipImageLayout);
25.
26. }
27.
28. @Override
29. public boolean onCreateOptionsMenu(Menu menu)
30. {
31. getMenuInflater().inflate(R.menu.main, menu);
32. return true;
33. }
34.
35. @Override
36. public boolean onOptionsItemSelected(MenuItem item)
37. {
38. switch (item.getItemId())
39. {
40. case R.id.id_action_clip:
41. Bitmap bitmap = mClipImageLayout.clip();
42.
43. ByteArrayOutputStream baos = new ByteArrayOutputStream();
44. bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
45. byte[] datas = baos.toByteArray();
46.
47. Intent intent = new Intent(this, ShowImageActivity.class);
48. intent.putExtra("bitmap", datas);
49. startActivity(intent);
50.
51. break;
52. }
53. return super.onOptionsItemSelected(item);
54. }
55.}

我们在menu里面体检了一个裁切的按钮,点击后把裁切好的图片传递给我们的ShowImageActivity

看一下眼menu的xml

[html] view plaincopy 01.<menu xmlns:android="http://schemas.android.com/apk/res/android" >
02.
03. <item
04. android:id="@+id/id_action_clip"
05. android:icon="@drawable/actionbar_clip_icon"
06. android:showAsAction="always|withText"
07. android:title="裁切"/>
08.
09.</menu>

、ShowImageActivity

[java] view plaincopy 01.package com.zhy.clippic;
02.
03.
04.import android.app.Activity;
05.import android.graphics.Bitmap;
06.import android.graphics.BitmapFactory;
07.import android.os.Bundle;
08.import android.widget.ImageView;
09.
10.
11.public class ShowImageActivity extends Activity
12.{
13. private ImageView mImageView;
14.
15.
16. @Override
17. protected void onCreate(Bundle savedInstanceState)
18. {
19. super.onCreate(savedInstanceState);
20. setContentView(R.layout.show);
21.
22.
23. mImageView = (ImageView) findViewById(R.id.id_showImage);
24. byte[] b = getIntent().getByteArrayExtra("bitmap");
25. Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
26. if (bitmap != null)
27. {
28. mImageView.setImageBitmap(bitmap);
29. }
30. }
31.}

layout/show.xml

[html] view plaincopy 01.<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
02. xmlns:tools="http://schemas.android.com/tools"
03. android:layout_width="match_parent"
04. android:layout_height="match_parent"
05. android:background="#ffffff" >
06.
07. <ImageView
08. android:id="@+id/id_showImage"
09. android:layout_width="wrap_content"
10. android:layout_height="wrap_content"
11. android:layout_centerInParent="true"
12. android:src="@drawable/tbug"
13. />
14.
15.</RelativeLayout>

最后我们把ClipImageLayout里面的mHorizontalPadding设置为50,贴个静态效果图~

在此输入图片描述

当然你还可以添加更多的功能。 end

转载于:https://my.oschina.net/bigfool007139/blog/403320

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值