本文出自 “阿敏其人” 簡書博客,轉載或引用請注明出處。
在自定義控件——初識自定義控件里面,我們已經大致介紹三種自定義控件,分別是
- 自制控件
- 組合控件
- 拓展控件
并且,我們已經對自制控件就繼承自View和繼承自ViewGroup進行了分析和最簡單deme展示。
熟能生巧,接下的幾篇文章,我們依然來進行自制控件。
在本篇里面,我們來進行自制簡單的開關按鈕。
有圖有真相,先看一下最終的效果圖。
馬上開工。
一、思路整理
從上面的圖片中,我們看出,這個自定義控件涉及到3張圖片,1張是黑色背景,1張是白色背景,另外一張就是一個圓形球的圖片。
思路:弄一個類繼承自View,比如叫做DiyToggleView,利用onDraw()方法里面,控制著三張圖片在對應的時刻顯示對應的圖片。
我們觸摸球狀圖片的時候,這張圖片會動起來,所以需要用到onTouchEvent,然后在這里面進行MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE和MotionEvent.ACTION_UP的判斷
在DiyToggleView里面,弄一些方法和回調接口給控件的使用者使用。
大概思路已經整理好了,現在開工。
二、準備三張圖片,然后在在繼承自View的自定義控件里面做出一個最簡單的開關的樣子
.
.
1、準備3張圖片
2、畫上簡單的開關樣子
這個開關我們繼承自View,然后復寫三個構造方法。
其實說到底,我們這個開關的自定義流程就是弄3個Bitmap,然后對點擊和滑動進行監聽,根據滑動的位置控制Bitmap是否顯示。
既然思路擺在這里,那么我們就先在onDraw畫上一個開關的樣子。
DiyToggleView
public class DiyToggleView extends View {
private Bitmap toggleBall;
private Bitmap toggleOnBg;
private Bitmap toggleOffBg;
public DiyToggleView(Context context) {
super(context);
}
public DiyToggleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 設置開關的滑動的球
* @param resId
*/
public void setToggleBallBitmap(int resId) {
toggleBall = BitmapFactory.decodeResource(getResources(), resId);
}
/**
* 設置開關的打開狀態的背景
* @param resId
*/
public void setToggleOnBgBitmap(int resId){
toggleOnBg = BitmapFactory.decodeResource(getResources(),resId);
}
/**
* 設置開關關閉狀態的背景
* @param resId
*/
public void setToggleOffBgBitmap(int resId){
toggleOffBg = BitmapFactory.decodeResource(getResources(),resId);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(toggleOnBg != null){
canvas.drawBitmap(toggleOnBg, 0, 0, null);
}
if(toggleBall != null){
canvas.drawBitmap(toggleBall,0,0,null);
}
}
// 我們需要精確控制這個開關的大小,所以必須復寫onMeasure方法
// 在onMeasure里面精確控制大小用到的是 setMeasuredDimension 這個方法
// 假設不復寫控制的大小,那么這個自動自定義View即使寬高都為wrap_content也會占據全屏
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = toggleOnBg.getWidth();
int measureHeight = toggleOnBg.getHeight();
setMeasuredDimension(measureWidth,measureHeight); // 控制View的大小的關鍵方法
}
}
.
.
MainActivity
public class MainActivity extends Activity {
private DiyToggleView mDtv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDtv = (DiyToggleView) findViewById(R.id.mDtv);
mDtv.setToggleBallBitmap(R.mipmap.switch_btn_ball);
mDtv.setToggleOnBgBitmap(R.mipmap.switch_btn_on);
mDtv.setToggleOffBgBitmap(R.mipmap.switch_btn_off);
}
}
.
.
activity_main
<?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"
tools:context="com.amqr.diytoggle.MainActivity">
<com.amqr.diytoggle.DiyToggleView
android:id="@+id/mDtv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#00ff00"
/>
</RelativeLayout>
畫好了
(綠色背景是故意設置上去的,便于標示)
三、利用 onTouchEvent和onDraw讓開關球可以被滑動
代碼都不用改動,只需要改動 DiyToggleView
public class DiyToggleView extends View {
private Bitmap toggleBall;
private Bitmap toggleOnBg;
private Bitmap toggleOffBg;
private int ballCurrentX; // 當前開關球所在的X
public DiyToggleView(Context context) {
super(context);
}
public DiyToggleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 設置開關的滑動的球
* @param resId
*/
public void setToggleBallBitmap(int resId) {
toggleBall = BitmapFactory.decodeResource(getResources(), resId);
}
/**
* 設置開關的打開狀態的背景
* @param resId
*/
public void setToggleOnBgBitmap(int resId){
toggleOnBg = BitmapFactory.decodeResource(getResources(),resId);
}
/**
* 設置開關關閉狀態的背景
* @param resId
*/
public void setToggleOffBgBitmap(int resId){
toggleOffBg = BitmapFactory.decodeResource(getResources(),resId);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(toggleOnBg != null){
canvas.drawBitmap(toggleOnBg, 0, 0, null);
}
if(toggleBall != null){
canvas.drawBitmap(toggleBall,ballCurrentX,0,null);
}
}
// 我們需要精確控制這個開關的大小,所以必須復寫onMeasure方法
// 在onMeasure里面精確控制大小用到的是 setMeasuredDimension 這個方法
// 假設不復寫控制的大小,那么這個自動自定義View即使寬高都為wrap_content也會占據全屏
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = toggleOnBg.getWidth();
int measureHeight = toggleOnBg.getHeight();
setMeasuredDimension(measureWidth,measureHeight); // 控制View的大小的關鍵方法
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
// 觸摸事件的down,move,up,按下,移動,松開
// getX()是觸摸的點與控件自身的距離
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
ballCurrentX = (int)(event.getX()+0.5f); // getX()代表當前,返回值是float,加上0.5是為了確保四舍五進進1
break;
case MotionEvent.ACTION_MOVE:
ballCurrentX = (int)(event.getX()+0.5f);
break;
case MotionEvent.ACTION_UP:
ballCurrentX = (int)(event.getX()+0.5f);
break;
}
invalidate(); // 重新繪制,可以理解為調用onDraw
return true; // 返回 true,代表當前View消費當前touch
}
}
簡單的滑動最好了,但是現在滑到中途釋放的時候沒有進行位置歸位判斷,而且還有越界問題。
四、釋放的位置歸和越界問題的解決
其實也簡單,就是添加兩個boolean值,判斷是否打開和是否觸摸
private boolean isOpen = true;
private boolean isOpen; // 作用是區分開 觸摸事件的 up
然后onDraw和onTouchEvent相互結合,isOpen和isOpen的值,做出相應的位置處理
最后我們寫了一個回調,讓調用者可以獲知當前的開關狀態和控制開關的狀態。
.
.
DiyToggleView
public class DiyToggleView extends View {
private Bitmap toggleBall;
private Bitmap toggleOnBg;
private Bitmap toggleOffBg;
private int ballCurrentX; // 當前開關球所在的X
private boolean isOpen = true;
private boolean isTouch; // 作用是區分開 觸摸事件的 up
private ToggleStaleListener toggleStaleListener;
/**
* 對開關狀態的回調操作
* @param toggleStaleListener
*/
public void setToggleState(ToggleStaleListener toggleStaleListener){
this.toggleStaleListener = toggleStaleListener;
}
public void setToggleState(boolean state){
isOpen = state;
invalidate();
}
public boolean getToggleState(){
return isOpen;
}
public DiyToggleView(Context context) {
super(context);
}
public DiyToggleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DiyToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 設置開關的滑動的球
*
* @param resId
*/
public void setToggleBallBitmap(int resId) {
toggleBall = BitmapFactory.decodeResource(getResources(), resId);
}
/**
* 設置開關的打開狀態的背景
*
* @param resId
*/
public void setToggleOnBgBitmap(int resId) {
toggleOnBg = BitmapFactory.decodeResource(getResources(), resId);
}
/**
* 設置開關關閉狀態的背景
*
* @param resId
*/
public void setToggleOffBgBitmap(int resId) {
toggleOffBg = BitmapFactory.decodeResource(getResources(), resId);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (toggleOnBg == null ||toggleOffBg==null|| toggleBall == null) {
return;
}
int ballWidth = toggleBall.getWidth();
int ballToRightX = toggleOnBg.getWidth() - ballWidth;
// 當處于觸摸事件的down和move狀態
if(isTouch){
if(isOpen){
canvas.drawBitmap(toggleOnBg, 0, 0, null);
// 開關球 不觸及邊界的自由活動范圍
if(ballCurrentX>ballWidth&&ballCurrentX<ballToRightX){
canvas.drawBitmap(toggleBall,ballCurrentX, 0, null);
}
// 當開關球與View的距離大于右側的球和邊框的距離,就停在右側
if (ballCurrentX>ballToRightX){
canvas.drawBitmap(toggleBall,ballToRightX, 0, null);
}
// 當開關球與View的距離小于左側球與邊距的邊框的距離,就停在左側
if (ballCurrentX < ballWidth) {
canvas.drawBitmap(toggleBall, 0, 0, null);
isOpen = true;
}
}else{
canvas.drawBitmap(toggleOffBg, 0, 0, null);
if(ballCurrentX>ballWidth&&ballCurrentX<ballToRightX){
canvas.drawBitmap(toggleBall,ballCurrentX, 0, null);
}
if (ballCurrentX > ballToRightX) {
canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
isOpen = false;
}
if(ballCurrentX < ballWidth){
canvas.drawBitmap(toggleBall, 0, 0, null);
}
}
// 當觸摸時間的up狀態被觸發
}else{
if(isOpen){
canvas.drawBitmap(toggleOnBg, 0, 0, null);
canvas.drawBitmap(toggleBall, 0, 0, null);
}else{
canvas.drawBitmap(toggleOffBg, 0, 0, null);
canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
}
}
}
// 我們需要精確控制這個開關的大小,所以必須復寫onMeasure方法
// 在onMeasure里面精確控制大小用到的是 setMeasuredDimension 這個方法
// 假設不復寫控制的大小,那么這個自動自定義View即使寬高都為wrap_content也會占據全屏
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = toggleOnBg.getWidth();
int measureHeight = toggleOnBg.getHeight();
setMeasuredDimension(measureWidth, measureHeight); // 控制View的大小的關鍵方法
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
// 觸摸事件的down,move,up,按下,移動,松開
// getX()是觸摸的點與控件自身的距離
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
isTouch = true;
ballCurrentX = (int)(event.getX()+0.5f); // getX()代表當前,返回值是float,加上0.5是為了確保四舍五進進1
break;
case MotionEvent.ACTION_MOVE:
isTouch = true;
ballCurrentX = (int) (event.getX() + 0.5f);
break;
case MotionEvent.ACTION_UP:
isTouch = false;
ballCurrentX = (int) (event.getX() + 0.5f);
if(ballCurrentX<toggleBall.getWidth()){
isOpen = true;
if(toggleStaleListener != null){
// 回調真正被執行
toggleStaleListener.toggleState(this,isOpen);
}
}
if(ballCurrentX>(toggleOnBg.getWidth()-toggleBall.getWidth())){
isOpen = false;
if(toggleStaleListener != null){
toggleStaleListener.toggleState(this,isOpen);
}
}
break;
}
invalidate(); // 重新繪制,可以理解為調用onDraw
return true; // 返回 true,代表當前View消費當前touch
}
// 回調接口
public interface ToggleStaleListener{
void toggleState(DiyToggleView view,boolean state);
}
}
上面的代碼其實主要看onDraw和onTouchEvent就好。
上面的onTouchEvent需要注意的是記得在后面加上invalidate();每次invalidate()被執行就會去重新調用一下onDraw
1、利用getX獲得球當前相對于控件的距離
不管是越界處理還是手指釋放時的位置歸正,都需要對球的位置進行判斷,那么怎么獲取球當前的位置呢?利用getX
這里我們有必要先來了解什么View的
getX()、getY()
getRawX()、getRawY()
這么幾個方法。
這幾個方法都是關于距離和點的方法。
先看圖
上結論:
getX()是觸摸的點與控件自身的距離
getRawX()是觸摸的點與屏幕的距離
(getX之后得到的是float類型的值,而不是int,所以我們加上0.5f保證帶小數的float轉換成int類型的時候都能夠進1,方便計算。)
我們利用getWidth可以得到背景的寬度。
利用getX可以得到當前我們的球當前按下的點距離背景的距離。
這兩者一結合,再加判斷,就可解決越界和位置歸正的問題
越界問題的解決
左側不越界
// 當開關球與View的距離小于左側球與邊距的邊框的距離,就停在左側
if (ballCurrentX < ballWidth) {
canvas.drawBitmap(toggleBall, 0, 0, null);
isOpen = true;
}
右側不越界
// 當開關球與View的距離大于右側的球和邊框的距離,就停在右側
if (ballCurrentX>ballToRightX){
canvas.drawBitmap(toggleBall,ballToRightX, 0, null);
}
位置歸正問題的解決
// 當觸摸時間的up狀態被觸發
}else{
if(isOpen){
canvas.drawBitmap(toggleOnBg, 0, 0, null);
canvas.drawBitmap(toggleBall, 0, 0, null);
}else{
canvas.drawBitmap(toggleOffBg, 0, 0, null);
canvas.drawBitmap(toggleBall, ballToRightX, 0, null);
}
}
設置回調接口,給使用者自主設置開關的狀態的權力
// 回調接口
public interface ToggleStaleListener{
void toggleState(DiyToggleView view,boolean state);
}
回調執行的地方
case MotionEvent.ACTION_UP:
isTouch = false;
ballCurrentX = (int) (event.getX() + 0.5f);
if(ballCurrentX<toggleBall.getWidth()){
isOpen = true;
if(toggleStaleListener != null){
// 回調真正被執行
toggleStaleListener.toggleState(this,isOpen);
}
}
if(ballCurrentX>(toggleOnBg.getWidth()-toggleBall.getWidth())){
isOpen = false;
if(toggleStaleListener != null){
toggleStaleListener.toggleState(this,isOpen);
}
}
break;
關于回調可以參考這里 說說安卓回調——CallBack
.
.
MainActivity 使用這個回調
public class MainActivity extends Activity {
private DiyToggleView mDtv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDtv = (DiyToggleView) findViewById(R.id.mDtv);
mDtv.setToggleBallBitmap(R.mipmap.switch_btn_ball);
mDtv.setToggleOnBgBitmap(R.mipmap.switch_btn_on);
mDtv.setToggleOffBgBitmap(R.mipmap.switch_btn_off);
mDtv.setToggleState(false);
mDtv.setToggleState(new DiyToggleView.ToggleStaleListener() {
@Override
public void toggleState(DiyToggleView view, boolean state) {
Toast.makeText(MainActivity.this,"當前的開關狀態是:"+mDtv.getToggleState(),Toast.LENGTH_SHORT).show();
}
});
}
}
至此完成。
另外一篇博文自制控件2 —— 自制控件 仿qq側滑菜單一文中,將繼續自制控件,歡迎點擊查閱。
本篇完。