Android如何自定义头像控件

技术分享

如上图效果:
效果分析

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

让我们来写代码吧~

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

ClipImageBorderView

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

我们准备按如下图绘制:
技术分享

代码:

[java] view plaincopy
01.package com.zhy.view;

  1. 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./**

  2. */
    15.public class ClipImageBorderView extends View
    16.{

  3. /**

  4. * 水平方向与View的边距 
    
  5. */  
    
  6. private int mHorizontalPadding = 20;

  7. /**

  8. * 垂直方向与View的边距 
    
  9. */  
    
  10. private int mVerticalPadding;

  11. /**

  12. * 绘制的矩形的宽度 
    
  13. */  
    
  14. private int mWidth;

  15. /**

  16. * 边框的颜色,默认为白色 
    
  17. */  
    
  18. private int mBorderColor = Color.parseColor(“#FFFFFF”);

  19. /**

  20. * 边框的宽度 单位dp 
    
  21. */  
    
  22. private int mBorderWidth = 1;

  23. private Paint mPaint;

  24. public ClipImageBorderView(Context context)

  25. {

  26.    this(context, null);  
    
  27. }

  28. public ClipImageBorderView(Context context, AttributeSet attrs)

  29. {

  30.    this(context, attrs, 0);  
    
  31. }

  32. public ClipImageBorderView(Context context, AttributeSet attrs, int defStyle)

  33. {

  34.    super(context, attrs, defStyle);  
    
  35.    // 计算padding的px  
    
  36.    mHorizontalPadding = (int) TypedValue.applyDimension(  
    
  37.            TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources()  
    
  38.                    .getDisplayMetrics());  
    
  39.    mBorderWidth = (int) TypedValue.applyDimension(  
    
  40.            TypedValue.COMPLEX_UNIT_DIP, mBorderWidth, getResources()  
    
  41.                    .getDisplayMetrics());  
    
  42.    mPaint = new Paint();  
    
  43.    mPaint.setAntiAlias(true);  
    
  44. }

  45. @Override

  46. protected void onDraw(Canvas canvas)

  47. {

  48.    super.onDraw(canvas);  
    
  49.    //计算矩形区域的宽度  
    
  50.    mWidth = getWidth() - 2 * mHorizontalPadding;  
    
  51.    //计算距离屏幕垂直边界 的边距  
    
  52.    mVerticalPadding = (getHeight() - mWidth) / 2;  
    
  53.    mPaint.setColor(Color.parseColor("#aa000000"));  
    
  54.    mPaint.setStyle(Style.FILL);  
    
  55.    // 绘制左边1  
    
  56.    canvas.drawRect(0, 0, mHorizontalPadding, getHeight(), mPaint);  
    
  57.    // 绘制右边2  
    
  58.    canvas.drawRect(getWidth() - mHorizontalPadding, 0, getWidth(),  
    
  59.            getHeight(), mPaint);  
    
  60.    // 绘制上边3  
    
  61.    canvas.drawRect(mHorizontalPadding, 0, getWidth() - mHorizontalPadding,  
    
  62.            mVerticalPadding, mPaint);  
    
  63.    // 绘制下边4  
    
  64.    canvas.drawRect(mHorizontalPadding, getHeight() - mVerticalPadding,  
    
  65.            getWidth() - mHorizontalPadding, getHeight(), mPaint);  
    
  66.    // 绘制外边框  
    
  67.    mPaint.setColor(mBorderColor);  
    
  68.    mPaint.setStrokeWidth(mBorderWidth);  
    
  69.    mPaint.setStyle(Style.STROKE);  
    
  70.    canvas.drawRect(mHorizontalPadding, mVerticalPadding, getWidth()  
    
  71.            - mHorizontalPadding, getHeight() - mVerticalPadding, mPaint);  
    
  72. }

  73. 94.}

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

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

布局文件:

[html] view plaincopy
01.<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”

  1. xmlns:tools=“http://schemas.android.com/tools”

  2. android:layout_width=“match_parent”

  3. android:layout_height=“match_parent”

  4. android:background=“@drawable/a” >

  5. <com.zhy.view.ClipImageBorderView

  6.    android:id="@+id/id_clipImageLayout"  
    
  7.    android:layout_width="fill_parent"  
    
  8.    android:layout_height="fill_parent" />  
    
  9. 12.

效果图:技术分享

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

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

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

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

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

部分代码:

[java] view plaincopy
01./**

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

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

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

5、不一样的自定义控件

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

[html] view plaincopy
01.<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”

  1. xmlns:tools=“http://schemas.android.com/tools”

  2. android:layout_width=“match_parent”

  3. android:layout_height=“match_parent”

  4. android:background=“#aaaaaa” >

  5. <com.zhy.view.ZoomImageView

  6.    android:id="@+id/id_zoomImageView"  
    
  7.    android:layout_width="fill_parent"  
    
  8.    android:layout_height="fill_parent"  
    
  9.    android:scaleType="matrix"  
    
  10.    android:src="@drawable/a" />  
    
  11. <com.zhy.view.ClipImageView

  12.    android:layout_width="fill_parent"  
    
  13.    android:layout_height="fill_parent" />  
    
  14. 18.

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

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

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

、ClipImageLayout

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

完整的ClipImageLayout代码:

[java] view plaincopy
01.package com.zhy.view;

  1. 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;

  2. 09.import com.zhy.clippic.R;
    10./**

    • zhy
  3. */
    15.public class ClipImageLayout extends RelativeLayout
    16.{

  4. private ClipZoomImageView mZoomImageView;

  5. private ClipImageBorderView mClipImageView;

  6. /**

  7. * 这里测试,直接写死了大小,真正使用过程中,可以提取为自定义属性 
    
  8. */  
    
  9. private int mHorizontalPadding = 20;

  10. public ClipImageLayout(Context context, AttributeSet attrs)

  11. {

  12.    super(context, attrs);  
    
  13.    mZoomImageView = new ClipZoomImageView(context);  
    
  14.    mClipImageView = new ClipImageBorderView(context);  
    
  15.    android.view.ViewGroup.LayoutParams lp = new LayoutParams(  
    
  16.            android.view.ViewGroup.LayoutParams.MATCH_PARENT,  
    
  17.            android.view.ViewGroup.LayoutParams.MATCH_PARENT);  
    
  18.    /** 
    
  19.     * 这里测试,直接写死了图片,真正使用过程中,可以提取为自定义属性 
    
  20.     */  
    
  21.    mZoomImageView.setImageDrawable(getResources().getDrawable(  
    
  22.            R.drawable.a));  
    
  23.    this.addView(mZoomImageView, lp);  
    
  24.    this.addView(mClipImageView, lp);  
    
  25.    // 计算padding的px  
    
  26.    mHorizontalPadding = (int) TypedValue.applyDimension(  
    
  27.            TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources()  
    
  28.                    .getDisplayMetrics());  
    
  29.    mZoomImageView.setHorizontalPadding(mHorizontalPadding);  
    
  30.    mClipImageView.setHorizontalPadding(mHorizontalPadding);  
    
  31. }

  32. /**

  33. * 对外公布设置边距的方法,单位为dp 
    
  34. *  
    
  35. * @param mHorizontalPadding 
    
  36. */  
    
  37. public void setHorizontalPadding(int mHorizontalPadding)

  38. {

  39.    this.mHorizontalPadding = mHorizontalPadding;  
    
  40. }

  41. /**

  42. * 裁切图片 
    
  43. *  
    
  44. * @return 
    
  45. */  
    
  46. public Bitmap clip()

  47. {

  48.    return mZoomImageView.clip();  
    
  49. }

  50. 75.}

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

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

6、用法

1、布局文件

[html] view plaincopy
01.<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”

  1. xmlns:tools=“http://schemas.android.com/tools”

  2. android:layout_width=“match_parent”

  3. android:layout_height=“match_parent”

  4. android:background=“#aaaaaa” >

  5. <com.zhy.view.ClipImageLayout

  6.    android:id="@+id/id_clipImageLayout"  
    
  7.    android:layout_width="fill_parent"  
    
  8.    android:layout_height="fill_parent" />  
    
  9. 12.

、MainActivity

[java] view plaincopy
01.package com.zhy.clippic;

  1. 03.import java.io.ByteArrayOutputStream;

  2. 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;

  3. 12.import com.zhy.view.ClipImageLayout;

  4. 14.public class MainActivity extends Activity
    15.{

  5. private ClipImageLayout mClipImageLayout;

  6. @Override

  7. protected void onCreate(Bundle savedInstanceState)

  8. {

  9.    super.onCreate(savedInstanceState);  
    
  10.    setContentView(R.layout.activity_main);  
    
  11.    mClipImageLayout = (ClipImageLayout) findViewById(R.id.id_clipImageLayout);  
    
  12. }

  13. @Override

  14. public boolean onCreateOptionsMenu(Menu menu)

  15. {

  16.    getMenuInflater().inflate(R.menu.main, menu);  
    
  17.    return true;  
    
  18. }

  19. @Override

  20. public boolean onOptionsItemSelected(MenuItem item)

  21. {

  22.    switch (item.getItemId())  
    
  23.    {  
    
  24.    case R.id.id_action_clip:  
    
  25.        Bitmap bitmap = mClipImageLayout.clip();  
    
  26.        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
    
  27.        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);  
    
  28.        byte[] datas = baos.toByteArray();  
    
  29.        Intent intent = new Intent(this, ShowImageActivity.class);  
    
  30.        intent.putExtra("bitmap", datas);  
    
  31.        startActivity(intent);  
    
  32.        break;  
    
  33.    }  
    
  34.    return super.onOptionsItemSelected(item);  
    
  35. }
    55.}

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

看一下眼menu的xml

[html] view plaincopy
01.

  1. <item

  2.    android:id="@+id/id_action_clip"  
    
  3.    android:icon="@drawable/actionbar_clip_icon"  
    
  4.    android:showAsAction="always|withText"  
    
  5.    android:title="裁切"/>  
    
  6. 09.

、ShowImageActivity

[java] view plaincopy
01.package com.zhy.clippic;

  1. 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;

  2. 11.public class ShowImageActivity extends Activity
    12.{

  3. private ImageView mImageView;

  4. @Override

  5. protected void onCreate(Bundle savedInstanceState)

  6. {

  7.    super.onCreate(savedInstanceState);  
    
  8.    setContentView(R.layout.show);  
    
  9.    mImageView = (ImageView) findViewById(R.id.id_showImage);  
    
  10.    byte[] b = getIntent().getByteArrayExtra("bitmap");  
    
  11.    Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);  
    
  12.    if (bitmap != null)  
    
  13.    {  
    
  14.        mImageView.setImageBitmap(bitmap);  
    
  15.    }  
    
  16. }
    31.}

layout/show.xml

[html] view plaincopy
01.<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android”

  1. xmlns:tools=“http://schemas.android.com/tools”

  2. android:layout_width=“match_parent”

  3. android:layout_height=“match_parent”

  4. android:background=“#ffffff” >

  5. <ImageView

  6.    android:id="@+id/id_showImage"  
    
  7.    android:layout_width="wrap_content"  
    
  8.    android:layout_height="wrap_content"  
    
  9.    android:layout_centerInParent="true"  
    
  10.    android:src="@drawable/tbug"  
    
  11.     />  
    
  12. 15.

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

技术分享

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

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。