Android 高仿微信頭像擷取 打造不一樣的自定義控制元件

發表於2015-06-05
轉載請表明出處:http://blog.csdn.net/lmj623565791/article/details/39761281,本文出自:【張鴻洋的部落格】

1、概述

前面已經寫了關於檢測手勢識別的文章,如果不瞭解可以參考:Android 手勢檢測實戰 打造支援縮放平移的圖片預覽效果(下)。首先本篇文章,將對之前部落格的ZoomImageView程式碼進行些許的修改與改善,然後用到我們的本篇部落格中去,實現仿微信的頭像擷取功能,當然了,個人覺得微信的擷取頭像功能貌似做得不太好,本篇部落格準備去其糟粕,取其精華;最後還會見識到不一樣的自定義控制元件的方式,也是在本人部落格中首次出現,如果有興趣可以讀完本篇部落格,希望可以啟到拋磚引玉的效果。

2、效果分析

1、效果圖:


我們來看看妹子的項鍊,嗯,妹子項鍊還是不錯的~

2、效果分析

根據上面的效果,我們目測需要自定義兩個控制元件,一個就是我們的可自由縮放移動的ImageView,一個就是那個白色的邊框;然後一起放置到一個RelativeLayout中;最後對外公佈一個裁剪的方法,返回一個Bitmap;

暫時的分析就這樣,下面我們來寫程式碼~

首先是白色框框那個自定義View,我們叫做ClipImageBorderView

3、ClipImageBorderView

分析下這個View,其實就是根據在螢幕中繪製一個正方形,正方形區域以外為半透明,繪製這個正方形需要與螢幕左右邊距有個邊距。

我們準備按如下圖繪製:


按順序在View的onDraw裡面繪製上圖中:1、2、3、4,四個半透明的區域,然後在中間正方形區域繪製一個正方形

下面看下程式碼:

[java] view plaincopy
  1. package com.zhy.view;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Canvas;  
  5. import android.graphics.Color;  
  6. import android.graphics.Paint;  
  7. import android.graphics.Paint.Style;  
  8. import android.util.AttributeSet;  
  9. 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(00, 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
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:background="@drawable/a"   >  
  6.   
  7.     <com.zhy.view.ClipImageBorderView  
  8.         android:id="@+id/id_clipImageLayout"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="fill_parent" />  
  11.   
  12. </RelativeLayout>  

效果圖:


故意放了個背景,沒撒用,就是為了能看出效果,可以看到我們的框框繪製的還是蠻不錯的~~嗯,這個框框距離螢幕左右兩側的距離應該抽取出來,嗯,後面再說~

4、ClipZoomImageView

我們準備對我們原先的ZoomImageView進行簡單的修改,修改的地方:
1、在onGlobalLayout方法中,如果圖片的寬或者高只要一個小於我們的正方形的邊長,我們會直接把較小的尺寸放大至正方形的邊長;如果圖片的寬和高都大於我們的正方形的邊長,我們僅僅把圖片移動到我們螢幕的中央,不做縮放處理;

2、根據步驟1,我們會獲得初始的縮放比例(預設為1.0f),然後SCALE_MID , 與 SCALE_MAX 分別為2倍和4倍的初始化縮放比例。

3、圖片在移動過程中的邊界檢測完全根據正方形的區域,圖片不會在移動過程中與正方形區域產生內邊距

4、對外公佈一個裁切的方法

部分程式碼:

[java] view plaincopy
  1. /** 
  2.      * 水平方向與View的邊距 
  3.      */  
  4.     private int mHorizontalPadding = 20;  
  5.     /** 
  6.      * 垂直方向與View的邊距 
  7.      */  
  8.     private int mVerticalPadding;  
  9.   
  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
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:background="#aaaaaa" >  
  6.   
  7.     <com.zhy.view.ZoomImageView  
  8.         android:id="@+id/id_zoomImageView"  
  9.         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中通過程式碼新增這兩個自定義的佈局,並且設定一些公用的屬性,具體我們就開始行動。

1、ClipImageLayout

我們自定義一個RelativeLayout叫做ClipImageLayout,用於放置我們的兩個自定義View,並且由ClipImageLayout進行設定邊距,然後傳給它內部的兩個View,這樣的話,跟使用者互動的就一個ClipImageLayout,使用者只需要設定一次邊距即可。

完整的ClipImageLayout程式碼:

[java] view plaincopy
  1. package com.zhy.view;  
  2.   
  3. import android.content.Context;  
  4. import android.graphics.Bitmap;  
  5. import android.util.AttributeSet;  
  6. import android.util.TypedValue;  
  7. import android.widget.RelativeLayout;  
  8.   
  9. 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
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:background="#aaaaaa"   >  
  6.   
  7.     <com.zhy.view.ClipImageLayout  
  8.         android:id="@+id/id_clipImageLayout"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="fill_parent" />  
  11.   
  12. </RelativeLayout>  

2、MainActivity

[java] view plaincopy
  1. package com.zhy.clippic;  
  2.   
  3. import java.io.ByteArrayOutputStream;  
  4.   
  5. import android.app.Activity;  
  6. import android.content.Intent;  
  7. import android.graphics.Bitmap;  
  8. import android.os.Bundle;  
  9. 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
  1. <menu xmlns:android="http://schemas.android.com/apk/res/android" >  
  2.   
  3.     <item  
  4.         android:id="@+id/id_action_clip"  
  5.         android:icon="@drawable/actionbar_clip_icon"  
  6.         android:showAsAction="always|withText"  
  7.         android:title="裁切"/>  
  8.   
  9. </menu>  

3、ShowImageActivity

[java] view plaincopy
  1. package com.zhy.clippic;  
  2.   
  3.   
  4. import android.app.Activity;  
  5. import android.graphics.Bitmap;  
  6. import android.graphics.BitmapFactory;  
  7. import android.os.Bundle;  
  8. import android.widget.ImageView;  
  9.   
  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
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:background="#ffffff" >  
  6.   
  7.     <ImageView  
  8.         android:id="@+id/id_showImage"  
  9.         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>  


好了,到此我們的 高仿微信頭像擷取功能 就已經結束了~~希望大家可以從本篇部落格中可以領悟到something~


最後我們把ClipImageLayout裡面的mHorizontalPadding設定為50,貼個靜態效果圖~



ok ~~


原始碼點選下載

相關文章