Android 高仿微信頭像截取 打造不一樣的自定義控件
轉載請表明出處: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,四個半透明的區域,然后在中間正方形區域繪制一個正方形
下面看下代碼:
package com.zhy.view; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; /** * @author zhy * */ public class ClipImageBorderView extends View { /** * 水平方向與View的邊距 */ private int mHorizontalPadding = 20; /** * 垂直方向與View的邊距 */ private int mVerticalPadding; /** * 繪制的矩形的寬度 */ private int mWidth; /** * 邊框的顏色,默認為白色 */ private int mBorderColor = Color.parseColor("#FFFFFF"); /** * 邊框的寬度 單位dp */ private int mBorderWidth = 1; private Paint mPaint; public ClipImageBorderView(Context context) { this(context, null); } public ClipImageBorderView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ClipImageBorderView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // 計算padding的px mHorizontalPadding = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources() .getDisplayMetrics()); mBorderWidth = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, mBorderWidth, getResources() .getDisplayMetrics()); mPaint = new Paint(); mPaint.setAntiAlias(true); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //計算矩形區域的寬度 mWidth = getWidth() - 2 * mHorizontalPadding; //計算距離屏幕垂直邊界 的邊距 mVerticalPadding = (getHeight() - mWidth) / 2; mPaint.setColor(Color.parseColor("#aa000000")); mPaint.setStyle(Style.FILL); // 繪制左邊1 canvas.drawRect(0, 0, mHorizontalPadding, getHeight(), mPaint); // 繪制右邊2 canvas.drawRect(getWidth() - mHorizontalPadding, 0, getWidth(), getHeight(), mPaint); // 繪制上邊3 canvas.drawRect(mHorizontalPadding, 0, getWidth() - mHorizontalPadding, mVerticalPadding, mPaint); // 繪制下邊4 canvas.drawRect(mHorizontalPadding, getHeight() - mVerticalPadding, getWidth() - mHorizontalPadding, getHeight(), mPaint); // 繪制外邊框 mPaint.setColor(mBorderColor); mPaint.setStrokeWidth(mBorderWidth); mPaint.setStyle(Style.STROKE); canvas.drawRect(mHorizontalPadding, mVerticalPadding, getWidth() - mHorizontalPadding, getHeight() - mVerticalPadding, mPaint); } }我們直接預設了一個水平方向的邊距,根據邊距計算出正方形的邊長,接下來就是按照上圖分別會1、2、3、4四個區域,最后就是繪制我們的正方形~~
代碼還是很簡單的~~我們的ClipImageBorderView就搞定了,我們決定來測試一下:
布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/a" > <com.zhy.view.ClipImageBorderView android:id="@+id/id_clipImageLayout" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </RelativeLayout>
效果圖:
故意放了個背景,沒撒用,就是為了能看出效果,可以看到我們的框框繪制的還是蠻不錯的~~嗯,這個框框距離屏幕左右兩側的距離應該抽取出來,嗯,后面再說~
4、ClipZoomImageView
我們準備對我們原先的ZoomImageView進行簡單的修改,修改的地方:
1、在onGlobalLayout方法中,如果圖片的寬或者高只要一個小于我們的正方形的邊長,我們會直接把較小的尺寸放大至正方形的邊長;如果圖片的寬和高都大于我們的正方形的邊長,我們僅僅把圖片移動到我們屏幕的中央,不做縮放處理;
2、根據步驟1,我們會獲得初始的縮放比例(默認為1.0f),然后SCALE_MID , 與 SCALE_MAX 分別為2倍和4倍的初始化縮放比例。
3、圖片在移動過程中的邊界檢測完全根據正方形的區域,圖片不會在移動過程中與正方形區域產生內邊距
4、對外公布一個裁切的方法
部分代碼:
/** * 水平方向與View的邊距 */ private int mHorizontalPadding = 20; /** * 垂直方向與View的邊距 */ private int mVerticalPadding; @Override public void onGlobalLayout() { if (once) { Drawable d = getDrawable(); if (d == null) return; Log.e(TAG, d.getIntrinsicWidth() + " , " + d.getIntrinsicHeight()); // 計算padding的px mHorizontalPadding = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources().getDisplayMetrics()); // 垂直方向的邊距 mVerticalPadding = (getHeight() - (getWidth() - 2 * mHorizontalPadding)) / 2; int width = getWidth(); int height = getHeight(); // 拿到圖片的寬和高 int dw = d.getIntrinsicWidth(); int dh = d.getIntrinsicHeight(); float scale = 1.0f; if (dw < getWidth() - mHorizontalPadding * 2 && dh > getHeight() - mVerticalPadding * 2) { scale = (getWidth() * 1.0f - mHorizontalPadding * 2) / dw; } if (dh < getHeight() - mVerticalPadding * 2 && dw > getWidth() - mHorizontalPadding * 2) { scale = (getHeight() * 1.0f - mVerticalPadding * 2) / dh; } if (dw < getWidth() - mHorizontalPadding * 2 && dh < getHeight() - mVerticalPadding * 2) { float scaleW = (getWidth() * 1.0f - mHorizontalPadding * 2) / dw; float scaleH = (getHeight() * 1.0f - mVerticalPadding * 2) / dh; scale = Math.max(scaleW, scaleH); } initScale = scale; SCALE_MID = initScale * 2; SCALE_MAX = initScale * 4; Log.e(TAG, "initScale = " + initScale); mScaleMatrix.postTranslate((width - dw) / 2, (height - dh) / 2); mScaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2); // 圖片移動至屏幕中心 setImageMatrix(mScaleMatrix); once = false; } } /** * 剪切圖片,返回剪切后的bitmap對象 * * @return */ public Bitmap clip() { Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); draw(canvas); return Bitmap.createBitmap(bitmap, mHorizontalPadding, mVerticalPadding, getWidth() - 2 * mHorizontalPadding, getWidth() - 2 * mHorizontalPadding); } /** * 邊界檢測 */ private void checkBorder() { RectF rect = getMatrixRectF(); float deltaX = 0; float deltaY = 0; int width = getWidth(); int height = getHeight(); // 如果寬或高大于屏幕,則控制范圍 if (rect.width() >= width - 2 * mHorizontalPadding) { if (rect.left > mHorizontalPadding) { deltaX = -rect.left + mHorizontalPadding; } if (rect.right < width - mHorizontalPadding) { deltaX = width - mHorizontalPadding - rect.right; } } if (rect.height() >= height - 2 * mVerticalPadding) { if (rect.top > mVerticalPadding) { deltaY = -rect.top + mVerticalPadding; } if (rect.bottom < height - mVerticalPadding) { deltaY = height - mVerticalPadding - rect.bottom; } } mScaleMatrix.postTranslate(deltaX, deltaY); }
這里貼出了改變的代碼,完整的代碼就不貼了,太長了,如果大家學習過前面的博客應該也會比較熟悉,若沒有也沒事,后面會提供源碼。
貼代碼的目的,第一讓大家看下我們改變了哪些;第二,我想暴露出我們代碼中的問題,我們設置了一個這樣的變量:mHorizontalPadding = 20;這個是手動和ClipImageBorderView里面的成員變量mHorizontalPadding 寫的一致,也就是說這個變量,兩個自定義的View都需要使用且需要相同的值,目前我們的做法,寫死且每個View各自定義一個。這種做法不用說,肯定不好,即使抽取成自定義屬性,兩個View都需要進行抽取,且用戶在使用的時候,還需要設置為一樣的值,總覺得有點強人所難~~
5、不一樣的自定義控件
現在我們考慮下:易用性。目前為止,其實我們的效果已經實現了,但是需要用戶這么寫布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#aaaaaa" > <com.zhy.view.ZoomImageView android:id="@+id/id_zoomImageView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:scaleType="matrix" android:src="@drawable/a" /> <com.zhy.view.ClipImageView android:layout_width="fill_parent" android:layout_height="fill_parent" /> </RelativeLayout>
然后這兩個類中都有一個mHorizontalPadding變量,且值一樣,上面也說過,即使抽取成自定義變量,也需要在布局文件中每個View中各寫一次。so, we need change . 這樣的耦合度太夸張了,且使用起來蹩腳。
于是乎,我決定把這兩個控件想辦法整到一起,用戶使用時只需要聲明一個控件:
怎么做呢,我們使用組合的思想來自定義控件,我們再聲明一個控件,繼承子RelativeLayout,然后在這個自定義RelativeLayout中通過代碼添加這兩個自定義的布局,并且設置一些公用的屬性,具體我們就開始行動。
1、ClipImageLayout
我們自定義一個RelativeLayout叫做ClipImageLayout,用于放置我們的兩個自定義View,并且由ClipImageLayout進行設置邊距,然后傳給它內部的兩個View,這樣的話,跟用戶交互的就一個ClipImageLayout,用戶只需要設置一次邊距即可。
完整的ClipImageLayout代碼:
package com.zhy.view; import android.content.Context; import android.graphics.Bitmap; import android.util.AttributeSet; import android.util.TypedValue; import android.widget.RelativeLayout; import com.zhy.clippic.R; /** * zhy * @author zhy * */ public class ClipImageLayout extends RelativeLayout { private ClipZoomImageView mZoomImageView; private ClipImageBorderView mClipImageView; /** * 這里測試,直接寫死了大小,真正使用過程中,可以提取為自定義屬性 */ private int mHorizontalPadding = 20; public ClipImageLayout(Context context, AttributeSet attrs) { super(context, attrs); mZoomImageView = new ClipZoomImageView(context); mClipImageView = new ClipImageBorderView(context); android.view.ViewGroup.LayoutParams lp = new LayoutParams( android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.MATCH_PARENT); /** * 這里測試,直接寫死了圖片,真正使用過程中,可以提取為自定義屬性 */ mZoomImageView.setImageDrawable(getResources().getDrawable( R.drawable.a)); this.addView(mZoomImageView, lp); this.addView(mClipImageView, lp); // 計算padding的px mHorizontalPadding = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, mHorizontalPadding, getResources() .getDisplayMetrics()); mZoomImageView.setHorizontalPadding(mHorizontalPadding); mClipImageView.setHorizontalPadding(mHorizontalPadding); } /** * 對外公布設置邊距的方法,單位為dp * * @param mHorizontalPadding */ public void setHorizontalPadding(int mHorizontalPadding) { this.mHorizontalPadding = mHorizontalPadding; } /** * 裁切圖片 * * @return */ public Bitmap clip() { return mZoomImageView.clip(); } }
可以看到,現在用戶需要使用頭像裁切功能只需要聲明下ClipImageLayout即可,完全避免了上述我們描述的問題,我們對用戶屏蔽了兩個真正實現的類。這個也是自定義控件的一種方式,希望可以借此拋磚引玉,大家能夠更加合理的設計出自己的控件~~
好了,我們的ClipImageLayout搞定以后,下面看下如何使用~
6、用法
1、布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#aaaaaa" > <com.zhy.view.ClipImageLayout android:id="@+id/id_clipImageLayout" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </RelativeLayout>
2、MainActivity
package com.zhy.clippic; import java.io.ByteArrayOutputStream; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import com.zhy.view.ClipImageLayout; public class MainActivity extends Activity { private ClipImageLayout mClipImageLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mClipImageLayout = (ClipImageLayout) findViewById(R.id.id_clipImageLayout); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.id_action_clip: Bitmap bitmap = mClipImageLayout.clip(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); byte[] datas = baos.toByteArray(); Intent intent = new Intent(this, ShowImageActivity.class); intent.putExtra("bitmap", datas); startActivity(intent); break; } return super.onOptionsItemSelected(item); } }
我們在menu里面體檢了一個裁切的按鈕,點擊后把裁切好的圖片傳遞給我們的ShowImageActivity
看一下眼menu的xml
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+id/id_action_clip" android:icon="@drawable/actionbar_clip_icon" android:showAsAction="always|withText" android:title="裁切"/> </menu>
3、ShowImageActivity
package com.zhy.clippic; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.widget.ImageView; public class ShowImageActivity extends Activity { private ImageView mImageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.show); mImageView = (ImageView) findViewById(R.id.id_showImage); byte[] b = getIntent().getByteArrayExtra("bitmap"); Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length); if (bitmap != null) { mImageView.setImageBitmap(bitmap); } } }
layout/show.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" > <ImageView android:id="@+id/id_showImage" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:src="@drawable/tbug" /> </RelativeLayout>
好了,到此我們的 高仿微信頭像截取功能 就已經結束了~~希望大家可以從本篇博客中可以領悟到something~
最后我們把ClipImageLayout里面的mHorizontalPadding設置為50,貼個靜態效果圖~
ok ~~
---------------------------------------------------------------------------------------------------------
我建了一個QQ群,方便大家交流。群號:55032675
----------------------------------------------------------------------------------------------------------
博主部分視頻已經上線,如果你不喜歡枯燥的文本,請猛戳(初錄,期待您的支持):
來自: http://blog.csdn.net//lmj623565791/article/details/39761281