1. 程式人生 > >Android 高仿微信頭像擷取 打造不一樣的自定義控制元件

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 ~~

博主部分視訊已經上線,如果你不喜歡枯燥的文字,請猛戳(初錄,期待您的支援):