1. 前言:
在平時的開發中,我們在顯示圖片是有時候需要顯示圓角圖片,我們應該都知道圓角顯示肯定是更加耗費內存和性能,會導致圖片的過度繪製等問題。但是有時候產品的設計就是這樣,我們開發也不得不做,本篇文章講一下最基本的圓角圖片實現方法:
2. 原理講解之Paint.setXfermode:
2.1 Paint.setXfermode就是本次實現圓角圖片的關鍵地方:
/**
* 我簡單理解爲設置畫筆在繪製時圖形堆疊時候的顯示模式
* SRC_IN:取兩層繪製交集。顯示上層。
*/
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
這個方法很複雜,我這裏只是爲本次使用做出解釋,這個方法大概的意思就是:設置畫筆在繪製時圖形堆疊時候的顯示模式。畫筆在繪製時圖形堆疊時候的顯示模式有16種之多,google也給出了圖文解釋:
看上圖就可以知道,就是兩個view堆疊在一起的時候是怎麼現實的,是顯示交集部分,非交集部分,交集部分的上層還是下層等等。
具體的PorterDuff.Mode請看:
1.PorterDuff.Mode.CLEAR
所繪製不會提交到畫布上。
2.PorterDuff.Mode.SRC
顯示上層繪製圖片
3.PorterDuff.Mode.DST
顯示下層繪製圖片
4.PorterDuff.Mode.SRC_OVER
正常繪製顯示,上下層繪製疊蓋。
5.PorterDuff.Mode.DST_OVER
上下層都顯示。下層居上顯示。
6.PorterDuff.Mode.SRC_IN
取兩層繪製交集。顯示上層。
7.PorterDuff.Mode.DST_IN
取兩層繪製交集。顯示下層。
8.PorterDuff.Mode.SRC_OUT
取上層繪製非交集部分。
9.PorterDuff.Mode.DST_OUT
取下層繪製非交集部分。
10.PorterDuff.Mode.SRC_ATOP
取下層非交集部分與上層交集部分
11.PorterDuff.Mode.DST_ATOP
取上層非交集部分與下層交集部分
12.PorterDuff.Mode.XOR
現實非交集部分
13.PorterDuff.Mode.DARKEN
14.PorterDuff.Mode.LIGHTEN
15.PorterDuff.Mode.MULTIPLY
16.PorterDuff.Mode.SCREEN
2.2 圓角圖片實現原理:
3. 代碼講解
自定義控件的基本步驟就是測量控件大小,確定控件位置,繪製控件,我們這個圓角圖片控件是不需要確定控件位置。
3.1 createRoundConerImage把源圖片圓角顯示:
原理上面講了,先繪製一個圓角矩形,再把我們的源圖片繪製在這個圓角矩形的畫布上,畫筆的顯示模式是SRC_IN,取上層交集部分,直接看代碼:
/**
* 根據給定的圖片和已經測量出來的寬高來繪製圓角圖形
* 原理:
* 基本原理就是先畫一個圓角的圖形出來,然後在圓角圖形上畫我們的源圖片,
* 圓角圖形跟我們的源圖片堆疊時我們取交集並顯示上層的圖形
* 原理就是這樣,很簡單。
*/
private Bitmap createRoundConerImage(Bitmap source){
final Paint paint = new Paint();
/**開啓抗鋸齒**/
paint.setAntiAlias(true);
/****/
Bitmap target = Bitmap.createBitmap(mWidth,mHeight, Bitmap.Config.ARGB_8888);
/**
* Construct a canvas with the specified bitmap to draw into. The bitmapmust be mutable
* 以bitmap對象創建一個畫布,則將內容都繪製在bitmap上,bitmap不得爲null;
*/
Canvas canvas = new Canvas(target);
/**新建一個矩形繪製區域,並給出左上角和右下角的座標**/
RectF rect = new RectF(0 , 0 ,mWidth ,mHeight);
/**
* 把圖片縮放成我們想要的大小
*/
source = Bitmap.createScaledBitmap(source,mWidth,mHeight,false);
/**在繪製矩形區域繪製用畫筆繪製一個圓角矩形**/
canvas.drawRoundRect(rect ,mRadius ,mRadius ,paint);
/**
* 我簡單理解爲設置畫筆在繪製時圖形堆疊時候的顯示模式
* SRC_IN:取兩層繪製交集。顯示上層。
*/
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(source ,0 ,0 ,paint);
/****/
return target;
}
需要注意,我們在繪製第二層,也就是我們的源圖片的時候,所需要的繪製矩形的寬高一定是跟我們測量的寬高是一直的,要是圖片的本身的寬高與測量得到的寬高不一致時,我們就對源圖片進行縮放,所以會有下面的代碼:
/**新建一個矩形繪製區域,並給出左上角和右下角的座標**/
/**mWidt和mHeight是測量得到的控件寬高 **/
RectF rect = new RectF(0 , 0 ,mWidth ,mHeight);
/**
* 把圖片縮放成我們想要的大小
*/
source = Bitmap.createScaledBitmap(source,mWidth,mHeight,false);
3.2 onMeasure測量控件大小:
/**
* 測量控件大小
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d("danxx" ,"onMeasure");
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**獲取寬高的測量模式**/
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
/**獲取寬高的尺寸**/
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
/**
* 測量寬度
*/
if(widthSpecMode == MeasureSpec.EXACTLY){ //寬爲具體值或者是填滿父控件就直接賦值 match_parent , accurate
mWidth = widthSpecSize;
}else{
/**圖片顯示時原始大小**/
int srcWidth = mSrc.getWidth() + getPaddingLeft() + getPaddingRight();
if(widthSpecMode == MeasureSpec.AT_MOST){ //wrap_content,子控件不能超過父控件,此時我們取傳遞過來的大小和圖片本身大小的小者
mWidth = Math.min(widthSpecSize , srcWidth);
}else{
//沒有要求,可以隨便大小
mWidth = srcWidth;
}
}
/**
* 測量高度,邏輯跟測量寬度是一樣的
*/
if(heightSpecMode == MeasureSpec.EXACTLY){ //match_parent , accurate
mHeight = heightSpecSize;
}else{
/**圖片顯示時原始大小**/
int srcHeigth = mSrc.getHeight() + getPaddingTop() + getPaddingBottom();
if(heightSpecMode == MeasureSpec.AT_MOST){ //wrap_content
mHeight = Math.min(heightSpecSize , srcHeigth);
}else{
//沒有要求,可以隨便大小
mHeight = srcHeigth;
}
}
setMeasuredDimension(mWidth ,mHeight);
}
寬高分兩次測量,下面簡單介紹一下控件的測量:
控件的測量就是處理我們在創建控件時設置的寬高大小,一般非爲三種情況,具體寬高值、填滿父控件,包含內容。在OnMeasure方法中我們使用MeasureSpec來獲取控件寬高到底是哪一種情況。
一個MeasureSpec封裝了父佈局傳遞給子佈局的佈局要求,每個MeasureSpec代表了一組寬度和高度的要求。
三種測量模式解釋:
- UNSPECIFIED:父佈局沒有給子佈局任何限制,子佈局可以任意大小。
- EXACTLY:父佈局決定子佈局的確切大小。不論子佈局多大,它都必須限制在這個界限裏。match_parent
- AT_MOST:此時子控件尺寸只要不超過父控件允許的最大尺寸,子佈局可以根據自己的大小選擇任意大小。wrap_content
簡單的映射關係:
- wrap_content -> MeasureSpec.AT_MOST
- match_parent -> MeasureSpec.EXACTLY
- 具體值 -> MeasureSpec.EXACTLY
3.3 onDraw繪製控件:
/**
* 繪製控件
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
Log.d("danxx" ,"onDraw");
// super.onDraw(canvas);
canvas.drawBitmap(createRoundConerImage(mSrc) ,0 ,0 ,null);
}
4. 全部代碼:
package danxx.library.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import danxx.library.R;
/**
* Created by Danxx on 2016/7/29.
* 最簡單的方式實現圓角圖片
*/
public class SampleCircleImageView extends View {
/**
* 默認圓角大小
*/
private static final int DEFUALT_RADIUS = 20;
/**
* 源圖片
*/
private Bitmap mSrc;
/**
* 圓角大小,默認爲20
*/
private int mRadius = DEFUALT_RADIUS;
/**
* 控件的寬度
*/
private int mWidth;
/**
* 控件的高度
*/
private int mHeight;
private Context mContext;
public SampleCircleImageView(Context context) {
super(context);
init(context ,null ,0);
}
public SampleCircleImageView(Context context ,Bitmap bitmap) {
super(context);
Log.d("danxx" ,"create SampleCircleImageView");
this.mSrc = bitmap;
init(context ,null ,0);
}
public SampleCircleImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context ,attrs ,0);
}
public SampleCircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context ,attrs ,defStyleAttr);
}
private void init(Context context ,AttributeSet attrs ,int defStyleAttr){
mContext = context;
if(attrs != null){
/**Load the styled attributes and set their properties**/
TypedArray typedArray = context.obtainStyledAttributes(attrs , R.styleable.SampleCircleImageView ,defStyleAttr ,0);
mSrc = BitmapFactory.decodeResource(context.getResources() ,typedArray.getResourceId(R.styleable.SampleCircleImageView_src ,0));
mRadius = (int) typedArray.getDimension(R.styleable.SampleCircleImageView_radius ,dp2px(DEFUALT_RADIUS));
typedArray.recycle();
}
}
/**
* 測量控件大小
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d("danxx" ,"onMeasure");
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 一個MeasureSpec封裝了父佈局傳遞給子佈局的佈局要求,每個MeasureSpec代表了一組寬度和高度的要求。
* 三種測量模式解釋:
* UNSPECIFIED:父佈局沒有給子佈局任何限制,子佈局可以任意大小。
* EXACTLY:父佈局決定子佈局的確切大小。不論子佈局多大,它都必須限制在這個界限裏。match_parent
* AT_MOST:此時子控件尺寸只要不超過父控件允許的最大尺寸,子佈局可以根據自己的大小選擇任意大小。wrap_content
*
* 簡單的映射關係:
* wrap_content -> MeasureSpec.AT_MOST
* match_parent -> MeasureSpec.EXACTLY
* 具體值 -> MeasureSpec.EXACTLY
*/
/**獲取寬高的測量模式**/
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
/**獲取寬高的尺寸**/
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
/**
* 測量寬度
*/
if(widthSpecMode == MeasureSpec.EXACTLY){ //寬爲具體值或者是填滿父控件就直接賦值 match_parent , accurate
mWidth = widthSpecSize;
}else{
/**圖片顯示時原始大小**/
int srcWidth = mSrc.getWidth() + getPaddingLeft() + getPaddingRight();
if(widthSpecMode == MeasureSpec.AT_MOST){ //wrap_content,子控件不能超過父控件,此時我們取傳遞過來的大小和圖片本身大小的小者
mWidth = Math.min(widthSpecSize , srcWidth);
}else{
//沒有要求,可以隨便大小
mWidth = srcWidth;
}
}
/**
* 測量高度,邏輯跟測量寬度是一樣的
*/
if(heightSpecMode == MeasureSpec.EXACTLY){ //match_parent , accurate
mHeight = heightSpecSize;
}else{
/**圖片顯示時原始大小**/
int srcHeigth = mSrc.getHeight() + getPaddingTop() + getPaddingBottom();
if(heightSpecMode == MeasureSpec.AT_MOST){ //wrap_content
mHeight = Math.min(heightSpecSize , srcHeigth);
}else{
//沒有要求,可以隨便大小
mHeight = srcHeigth;
}
}
setMeasuredDimension(mWidth ,mHeight);
}
/**
* 繪製控件
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
Log.d("danxx" ,"onDraw");
// super.onDraw(canvas);
canvas.drawBitmap(createRoundConerImage(mSrc) ,0 ,0 ,null);
}
/**
* 設置圓角大小
* @param radius
*/
public void setRadius(int radius){
this.mRadius = radius;
}
/**
* 設置圖片
* @param bitmap
*/
public void setSrc(Bitmap bitmap){
this.mSrc = bitmap;
}
/**
* 根據給定的圖片和已經測量出來的寬高來繪製圓角圖形
* 原理:
* 基本原理就是先畫一個圓角的圖形出來,然後在圓角圖形上畫我們的源圖片,
* 圓角圖形跟我們的源圖片堆疊時我們取交集並顯示上層的圖形
* 原理就是這樣,很簡單。
*/
private Bitmap createRoundConerImage(Bitmap source){
final Paint paint = new Paint();
/**開啓抗鋸齒**/
paint.setAntiAlias(true);
/****/
Bitmap target = Bitmap.createBitmap(mWidth,mHeight, Bitmap.Config.ARGB_8888);
/**
* Construct a canvas with the specified bitmap to draw into. The bitmapmust be mutable
* 以bitmap對象創建一個畫布,則將內容都繪製在bitmap上,bitmap不得爲null;
*/
Canvas canvas = new Canvas(target);
/**新建一個矩形繪製區域,並給出左上角和右下角的座標**/
RectF rect = new RectF(0 , 0 ,mWidth ,mHeight);
/**
* 把圖片縮放成我們想要的大小
*/
source = Bitmap.createScaledBitmap(source,mWidth,mHeight,false);
/**在繪製矩形區域繪製用畫筆繪製一個圓角矩形**/
canvas.drawRoundRect(rect ,mRadius ,mRadius ,paint);
/**
* 我簡單理解爲設置畫筆在繪製時圖形堆疊時候的顯示模式
* SRC_IN:取兩層繪製交集。顯示上層。
*/
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(source ,0 ,0 ,paint);
/****/
return target;
}
protected int sp2px(float spValue) {
final float fontScale = mContext.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
protected int dp2px(float dp) {
final float scale = mContext.getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
}
}
5. 效果圖和源碼地址:
GitHub源碼地址https://github.com/Dawish
三種寬高設置模式都有:
xml佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:danxx="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:padding="10dp"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1. 180*120:"/>
<danxx.library.widget.SampleCircleImageView
android:layout_marginTop="6dp"
android:layout_width="180dp"
android:layout_height="120dp"
danxx:src="@drawable/image"
danxx:radius="10dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="2. wrap_content*wrap_content:"/>
<danxx.library.widget.SampleCircleImageView
android:layout_marginTop="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
danxx:src="@drawable/vp1"
danxx:radius="10dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="3. match_parent*wrap_content:"/>
<danxx.library.widget.SampleCircleImageView
android:layout_marginTop="6dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
danxx:src="@drawable/vp2"
danxx:radius="10dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="4. wrap_content*wrap_content:"/>
<danxx.library.widget.SampleCircleImageView
android:layout_marginTop="6dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
danxx:src="@drawable/test2"
danxx:radius="10dp"/>
</LinearLayout>
</ScrollView>
attrs文件:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SampleCircleImageView">
<!-- 源圖片 -->
<attr name="src" format="reference"></attr>
<!-- 圓角大小 -->
<attr name="radius" format="dimension"></attr>
</declare-styleable>
</resources>