Android進階之自定義View實戰(一)仿iOS UISwitch控制元件實現
一.引言
個人覺得,自定義View一直是Android開發最變換莫測、最難掌握、最具吸引力的地方。因為它涉及到的知識點比較多,想在實際應用中駕輕就熟,由淺入深,你需要掌握以下知識點:
1. View的繪製機制以及Canvas、Paint、Rect等的常用方法;
2. View的測量及佈局機制:熟悉View的測量模式以及對實際寬高的影響;熟悉對view位置的影響因素,如:layout/onLayout方法、LayoutParams、TransationX/Y。
3. View/ViewGroup的事件分發、攔截及消費機制
4. View的滾動機制:Scroller的使用
接下來的幾篇部落格,我會給大家分享自己專案中的典型案例,讓大家明白自定義View的常用套路。這篇部落格主要介紹如何通過Canvas、Paint結合屬性動畫實現一個高仿蘋果UISwitch的控制元件。
二.案例分析
有關Canvas、Paint的學習資料網上有很多,這裡就不再贅述了。下面直接進入主題,看看蘋果上UISwitch長啥樣子吧。
可以看到,按鈕主要包括以下元素:
1. 跑道形狀的底板
2. 可變顏色的滑槽
3. 圓形手柄
4. 底板和手柄的深色邊框
這裡的深色邊框可以有兩種方式實現:1.Path 2.繪製兩個圖層,二者疊加實現。方法一的主要工作在於確定跑道形狀的路徑,運算量較大。方法二簡單粗暴,稍有不足的做了冗餘的繪製。個人傾向於後者。經過分解,於是就有了繪製的流程:
1. 深色底板的繪製(這個底板顏色也就是邊框的顏色);
2.灰色底板的繪製,這個底板顏色可變,size比深色底板小一個邊框的寬度
3.手柄的繪製,深色圓盤和白色圓盤組成帶邊框的手柄
基本形狀的繪製流程弄清楚之後,下面還有另外一個問題,開關切換時,手柄移動的同時,手柄槽顏色也從白色漸變到綠色,這個功能怎麼實現呢?這個開關的切換過程包含了兩個屬性的漸變:手柄位置和手柄槽顏色,很容易讓人想到屬性動畫,是不是?不熟悉的,請看看上篇部落格:
繪製和平滑切換的問題解決了之後,後面的點選事件和回撥處理就so easy啦?下面看看程式碼實現吧。
三.範例程式碼
AppleSwitch程式碼實現:
package com.star.appletogglebutton;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.OvershootInterpolator;
/**
* Created by kakaxicm on 16/5/17.
*/
public class AppleSwitch extends View {
private final int BORDER_WIDTH = 2;//邊框寬度
private int mBasePlaneColor = Color.GRAY;//地盤顏色
private int mOpenSlotColor = Color.parseColor("#4ebb7f");
private int mOffSlotColor = Color.parseColor("#dadbda");//關閉時手柄滑動槽的顏色
private int mSlotColor;
private RectF mRect = new RectF();
//繪製引數
private float mBackPlaneRadius;//底板的圓形半徑
private float mSpotRadius;//手柄半徑
private float spotStartX;//手柄的起始X位置,切換時平移改變它
private float mSpotY;//手柄的起始X位置,不變
private float mOffSpotX;//關閉時,手柄的水平位置
private Paint mPaint;//畫筆
private boolean mIsToggleOn;//開關標記
private OnToggleListener mOnToggleListener;//toggle事件監聽
interface OnToggleListener{
void onSwitchChangeListener(boolean switchState);
}
public AppleSwitch(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mIsToggleOn){
toggleOff();
}else {
toggleOn();
}
mIsToggleOn = !mIsToggleOn;
if(mOnToggleListener != null){
mOnToggleListener.onSwitchChangeListener(mIsToggleOn);
}
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = wSize;
int resultHeight = hSize;
Resources r = Resources.getSystem();
//lp = wrapcontent時 指定預設值
if(wMode == MeasureSpec.AT_MOST){
resultWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 50, r.getDisplayMetrics());
}
if(hMode == MeasureSpec.AT_MOST){
resultHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
}
setMeasuredDimension(resultWidth, resultHeight);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mBackPlaneRadius = Math.min(getWidth(), getHeight()) * 0.5f;
mSpotRadius = mBackPlaneRadius - BORDER_WIDTH;
spotStartX = 0;
mSpotY = 0;
mOffSpotX = getMeasuredWidth() - mBackPlaneRadius*2;
mSlotColor = mOffSlotColor;
}
@Override
protected void onDraw(Canvas canvas) {
//畫底板
mRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
mPaint.setColor(mBasePlaneColor);
canvas.drawRoundRect(mRect, mBackPlaneRadius, mBackPlaneRadius, mPaint);
//畫手柄的槽
mRect.set(BORDER_WIDTH, BORDER_WIDTH, getMeasuredWidth() - BORDER_WIDTH, getMeasuredHeight() - BORDER_WIDTH);
mPaint.setColor(mSlotColor);
canvas.drawRoundRect(mRect, mSpotRadius, mSpotRadius, mPaint);
//手柄包括包括兩部分,深色底板和白板,這樣做的目的是使圓盤具有邊框
//手柄的底盤
mRect.set(spotStartX, mSpotY, spotStartX+mBackPlaneRadius*2, mSpotY+mBackPlaneRadius*2);
mPaint.setColor(mBasePlaneColor);
canvas.drawRoundRect(mRect, mBackPlaneRadius, mBackPlaneRadius, mPaint);
//手柄的圓板
mRect.set(spotStartX+BORDER_WIDTH, mSpotY+BORDER_WIDTH, mSpotRadius*2+spotStartX+BORDER_WIDTH, mSpotRadius*2+mSpotY+BORDER_WIDTH);
mPaint.setColor(Color.WHITE);
canvas.drawRoundRect(mRect, mSpotRadius, mSpotRadius, mPaint);
}
public float getSpotStartX() {
return spotStartX;
}
public void setSpotStartX(float spotStartX) {
this.spotStartX = spotStartX;
}
/**
* 計算切換時的手柄槽的顏色
* @param fraction 動畫播放進度
* @param startColor 起始顏色
* @param endColor 終止顏色
*/
public void calculateColor(float fraction, int startColor, int endColor){
final int fb = Color.blue(startColor);
final int fr = Color.red(startColor);
final int fg = Color.green(startColor);
final int tb = Color.blue(endColor);
final int tr = Color.red(endColor);
final int tg = Color.green(endColor);
//RGB三通道線性漸變
int sr = (int) (fr + fraction*(tr - fr));
int sg = (int) (fg + fraction*(tg - fg));
int sb = (int) (fb + fraction*(tb - fb));
//範圍限定
sb = clamp(sb, 0, 255);
sr = clamp(sr, 0, 255);
sg = clamp(sg, 0, 255);
mSlotColor = Color.rgb(sr, sg, sb);
}
private int clamp(int value, int low, int high) {
return Math.min(Math.max(value, low), high);
}
//關閉
public void toggleOn(){
//手柄槽顏色漸變和手柄滑動通過屬性動畫來實現
ObjectAnimator animator = ObjectAnimator.ofFloat(this,"spotStartX", 0, mOffSpotX);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new OvershootInterpolator(0.5f));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
calculateColor(fraction, mOffSlotColor, mOpenSlotColor);
invalidate();
}
});
}
//開啟
public void toggleOff(){
//手柄槽顏色漸變和手柄滑動通過屬性動畫來實現
ObjectAnimator animator = ObjectAnimator.ofFloat(this,"spotStartX",mOffSpotX, 0);
animator.setDuration(300);
animator.start();
animator.setInterpolator(new OvershootInterpolator(0.5f));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
calculateColor(fraction, mOpenSlotColor, mOffSlotColor);
invalidate();
}
});
}
public boolean getSwitchState(){
return mIsToggleOn;
}
public void setToggle(boolean state){
mIsToggleOn = state;
if(mIsToggleOn){
toggleOff();
}else {
toggleOn();
}
}
public void setOnToggleListener(OnToggleListener listener){
mOnToggleListener = listener;
}
}
說明:
1.在onDraw方法裡面的邊框可以用繪製Path代替,繪製圓形也可以直接用drawCircle方法代替,看個人喜好
2.onMeasure方法裡面設定了預設的size,否則,在它和父View的LayoutParams均配置為wrap_content時,會充滿父view。這裡的機制會在後面的部落格詳細說明.
3.下面的程式碼
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
calculateColor(fraction, mOffSlotColor, mOpenSlotColor);
invalidate();
}
});
實現了手柄位置和槽的顏色漸變,當然也可以在setSpotStartX中實現,只是顏色漸變所需要的參量fraction還要計算,所以就放在動畫監聽裡了。
4.calculateColor方法通過RGB三通道顏色漸變與合成來實現顏色的平滑漸變
5.自定義屬性網上資料也有很多,這裡寫死了開關狀態的顏色。
通過這個例項,希望能讓大家覺得:原來一些簡單的控制元件也是just so so嘛!