Android 高仿微信頭像擷取 打造不一樣的自定義控制元件
1、概述
前面已經寫了關於檢測手勢識別的文章,如果不瞭解可以參考:Android 手勢檢測實戰 打造支援縮放平移的圖片預覽效果(下)。首先本篇文章,將對之前部落格的ZoomImageView程式碼進行些許的修改與改善,然後用到我們的本篇部落格中去,實現仿微信的頭像擷取功能,當然了,個人覺得微信的擷取頭像功能貌似做得不太好,本篇部落格準備去其糟粕,取其精華;最後還會見識到不一樣的自定義控制元件的方式,也是在本人部落格中首次出現,如果有興趣可以讀完本篇部落格,希望可以啟到拋磚引玉的效果。
2、效果分析
1、效果圖:
我們來看看妹子的項鍊,嗯,妹子項鍊還是不錯的~
2、效果分析
根據上面的效果,我們目測需要自定義兩個控制元件,一個就是我們的可自由縮放移動的ImageView,一個就是那個白色的邊框;然後一起放置到一個RelativeLayout中;最後對外公佈一個裁剪的方法,返回一個Bitmap;
暫時的分析就這樣,下面我們來寫程式碼~
首先是白色框框那個自定義View,我們叫做ClipImageBorderView
3、ClipImageBorderView
分析下這個View,其實就是根據在螢幕中繪製一個正方形,正方形區域以外為半透明,繪製這個正方形需要與螢幕左右邊距有個邊距。
我們準備按如下圖繪製:
按順序在View的onDraw裡面繪製上圖中:1、2、3、4,四個半透明的區域,然後在中間正方形區域繪製一個正方形
下面看下程式碼:
我們直接預設了一個水平方向的邊距,根據邊距計算出正方形的邊長,接下來就是按照上圖分別會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); } }
程式碼還是很簡單的~~我們的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 ~~
博主部分視訊已經上線,如果你不喜歡枯燥的文字,請猛戳(初錄,期待您的支援):