自定義滑動按鈕為例圖文剖析Android自定義View繪制
自定義View一直是橫在Android開發(fā)者面前的一道坎。
一、View和ViewGroup的關(guān)系
從View和ViewGroup的關(guān)系來看,ViewGroup繼承View。
View的子類,多是功能型的控件,提供繪制的樣式,比如imageView,TextView等,而ViewGroup的子類,多用于管理控件的大小,位置,如LinearLayout,RelativeLayout等,從下圖可以看出

從實際應(yīng)用中看,他們又是組合關(guān)系,我們在布局中,常常是一個ViewGroup嵌套多個ViewGroup或View,而被嵌套的ViewGroup又會嵌套多個ViewGroup或View
如下

二、View的繪制流程
從View源碼來看,主要關(guān)系三個方法:
1、measure():測量
一個final方法,控制控件的大小
2、layout():布局
用來控制自己的布局位置
有相對性,只相對于自己的父類布局,不關(guān)心祖宗布局
3、draw():繪制
用來控制控件的顯示樣式
流程: 流程 measure --> layout --> draw
對應(yīng)于我們要實現(xiàn)的方法是
onMeasure()
onLayout()
onDraw()
實際繪制中,我們的思考順序一般是這樣的:
是否需要控制控件的大小-->是-->onMeasure()
(1)如果這個自定義view不是ViewGroup,onMeasure()方法調(diào)用setMeasureDeminsion(width,height):用來設(shè)置自己的大小
(2)如果是ViewGroup,onMeasure()方法調(diào)用 ,child.measure()測量孩子的大小,給出孩子的期望大小值,之后-->setMeasureDeminsion(width,height):用來設(shè)置自己的大小
是否需要控制控件的擺放位置-->是 -->onLayout ()
是否需要控制控件的樣子-->是 -->onDraw ()-->canvas的繪制
下面是我繪制的流程圖:

下面以自定義滑動按鈕為例,說明自定義View的繪制流程
我們期待實現(xiàn)這樣的效果:

拖動或點擊按鈕,開關(guān)向右滑動,變成

其中開關(guān)能隨著手指的觸摸滑動到相應(yīng)位置,直到最后才固定在開關(guān)位置上
新建一個類繼承自View,實現(xiàn)其兩個構(gòu)造方法
public class SwitchButtonView extends View {
public SwitchButtonView(Context context) {
this(context, null);
}
public SwitchButtonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
drawable資源中添加這兩張圖片


借此,我們可以用onMeasure()確定這個控件的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mSwitchButton = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
mSlideButton = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button_background);
setMeasuredDimension(mSwitchButton.getWidth(), mSwitchButton.getHeight());
}
這個控件并不需要控制其擺放位置,略過onLayout();
接下來onDraw()確定其形狀。
但我們需要根據(jù)我們點擊控件的不同行為來確定形狀,這需要重寫onTouchEvent()
其中的邏輯是:
當(dāng)按下時,觸發(fā)事件MotionEvent.Action_Down,若此時狀態(tài)為關(guān):
(1)若手指觸摸點(通過event.getX()得到)在按鈕的 中線右側(cè),按鈕向右滑動一段距離(event.getX()與開關(guān)控件一半寬度之差,具體看下文源碼)
(2)若手指觸摸點在按鈕中線左側(cè),按鈕依舊處于最左(即“關(guān)”的狀態(tài))。
若此時狀態(tài)為開:
(1)若手指觸摸點在按鈕中線左側(cè),按鈕向左滑動一段距離
(2)若手指觸摸點在按鈕中線右側(cè),按鈕依舊處于最右(即“開”的狀態(tài))
當(dāng)滑動時,觸發(fā)時間MotionEvent.Action_MOVE,邏輯與按下時一致
注意,onTouchEvent()需要設(shè)置返回值 為 Return true,否則無法響應(yīng)滑動事件
當(dāng)手指收起時,若開關(guān)中線位于整個控件中線左側(cè),設(shè)置狀態(tài)為關(guān),反之,設(shè)置為開。
具體源碼如下所示:(還對外提供了一個暴露此時開關(guān)狀態(tài)的接口)
自定義View部分
package com.lian.switchtogglebutton;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* Created by lian on 2016/3/20.
*/
public class SwitchButtonView extends View {
private static final int STATE_NULL = 0;//默認狀態(tài)
private static final int STATE_DOWN = 1;
private static final int STATE_MOVE = 2;
private static final int STATE_UP = 3;
private Bitmap mSlideButton;
private Bitmap mSwitchButton;
private Paint mPaint = new Paint();
private int buttonState = STATE_NULL;
private float mDistance;
private boolean isOpened = false;
private onSwitchListener mListener;
public SwitchButtonView(Context context) {
this(context, null);
}
public SwitchButtonView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mSwitchButton = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
mSlideButton = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button_background);
setMeasuredDimension(mSwitchButton.getWidth(), mSwitchButton.getHeight());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mSwitchButton!= null){
canvas.drawBitmap(mSwitchButton, 0, 0, mPaint);
}
//buttonState的值在onTouchEvent()中確定
switch (buttonState){
case STATE_DOWN:
case STATE_MOVE:
if (!isOpened){
float middle = mSlideButton.getWidth() / 2f;
if (mDistance > middle) {
float max = mSwitchButton.getWidth() - mSlideButton.getWidth();
float left = mDistance - middle;
if (left >= max) {
left = max;
}
canvas.drawBitmap(mSlideButton,left,0,mPaint);
}
else {
canvas.drawBitmap(mSlideButton,0,0,mPaint);
}
}else{
float middle = mSwitchButton.getWidth() - mSlideButton.getWidth() / 2f;
if (mDistance < middle){
float left = mDistance-mSlideButton.getWidth()/2f;
float min = 0;
if (left < 0){
left = min;
}
canvas.drawBitmap(mSlideButton,left,0,mPaint);
}else{
canvas.drawBitmap(mSlideButton,mSwitchButton.getWidth()-mSlideButton.getWidth(),0,mPaint);
}
}
break;
case STATE_NULL:
case STATE_UP:
if (isOpened){
Log.d("開關(guān)","開著的");
canvas.drawBitmap(mSlideButton,mSwitchButton.getWidth()-mSlideButton.getWidth(),0,mPaint);
}else{
Log.d("開關(guān)","關(guān)著的");
canvas.drawBitmap(mSlideButton,0,0,mPaint);
}
break;
default:
break;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mDistance = event.getX();
Log.d("DOWN","按下");
buttonState = STATE_DOWN;
invalidate();
break;
case MotionEvent.ACTION_MOVE:
buttonState = STATE_MOVE;
mDistance = event.getX();
Log.d("MOVE","移動");
invalidate();
break;
case MotionEvent.ACTION_UP:
mDistance = event.getX();
buttonState = STATE_UP;
Log.d("UP","起開");
if (mDistance >= mSwitchButton.getWidth() / 2f){
isOpened = true;
}else {
isOpened = false;
}
if (mListener != null){
mListener.onSwitchChanged(isOpened);
}
invalidate();
break;
default:
break;
}
return true;
}
public void setOnSwitchListener(onSwitchListener listener){
this.mListener = listener;
}
public interface onSwitchListener{
void onSwitchChanged(boolean isOpened);
}
}
DemoActivity:
package com.lian.switchtogglebutton;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SwitchButtonView switchButtonView = (SwitchButtonView) findViewById(R.id.switchbutton);
switchButtonView.setOnSwitchListener(new SwitchButtonView.onSwitchListener() {
@Override
public void onSwitchChanged(boolean isOpened) {
if (isOpened) {
Toast.makeText(MainActivity.this, "打開", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(MainActivity.this, "關(guān)閉", Toast.LENGTH_SHORT).show();
}
}
});
}
}
布局:
<?xml version="1.0" encoding="utf-8"?> <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:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.lian.switchtogglebutton.MainActivity"> <com.lian.switchtogglebutton.SwitchButtonView android:id="@+id/switchbutton" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout>
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助。
相關(guān)文章
Android自定義控件實現(xiàn)帶文字提示的SeekBar
這篇文章主要給大家介紹了關(guān)于Android自定義控件實現(xiàn)帶文字提示的SeekBar的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12
Android分頁中顯示出下面翻頁的導(dǎo)航欄的布局實例代碼
這篇文章主要介紹了Android分頁中顯示出下面翻頁的導(dǎo)航欄的布局實例代碼,需要的朋友可以參考下2017-04-04
Android EditText實現(xiàn)分割輸入內(nèi)容
這篇文章主要為大家詳細介紹了Android EditText實現(xiàn)分割輸入內(nèi)容的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04
Android?RecyclerView曝光采集的實現(xiàn)方法
這篇文章主要為大家詳細介紹了Android?RecyclerView曝光采集的實現(xiàn)方法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01
android計算器實現(xiàn)兩位數(shù)的加減乘除
這篇文章主要為大家詳細介紹了android計算器實現(xiàn)兩位數(shù)的加減乘除,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-03-03
Android Popupwindow彈出窗口的簡單使用方法
這篇文章主要為大家詳細介紹了Android Popupwindow彈出窗口的簡單使用方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07

