自定义滑动按钮为例图文剖析Android自定义View绘制
作者:C_L 发布时间:2023-06-05 01:34:34
自定义View一直是横在Android开发者面前的一道坎。
一、View和ViewGroup的关系
从View和ViewGroup的关系来看,ViewGroup继承View。
View的子类,多是功能型的控件,提供绘制的样式,比如imageView,TextView等,而ViewGroup的子类,多用于管理控件的大小,位置,如LinearLayout,RelativeLayout等,从下图可以看出
从实际应用中看,他们又是组合关系,我们在布局中,常常是一个ViewGroup嵌套多个ViewGroup或View,而被嵌套的ViewGroup又会嵌套多个ViewGroup或View
如下
二、View的绘制流程
从View源码来看,主要关系三个方法:
1、measure():测量
一个final方法,控制控件的大小
2、layout():布局
用来控制自己的布局位置
有相对性,只相对于自己的父类布局,不关心祖宗布局
3、draw():绘制
用来控制控件的显示样式
流程: 流程 measure --> layout --> draw
对应于我们要实现的方法是
onMeasure()
onLayout()
onDraw()
实际绘制中,我们的思考顺序一般是这样的:
是否需要控制控件的大小-->是-->onMeasure()
(1)如果这个自定义view不是ViewGroup,onMeasure()方法调用setMeasureDeminsion(width,height):用来设置自己的大小
(2)如果是ViewGroup,onMeasure()方法调用 ,child.measure()测量孩子的大小,给出孩子的期望大小值,之后-->setMeasureDeminsion(width,height):用来设置自己的大小
是否需要控制控件的摆放位置-->是 -->onLayout ()
是否需要控制控件的样子-->是 -->onDraw ()-->canvas的绘制
下面是我绘制的流程图:
下面以自定义滑动按钮为例,说明自定义View的绘制流程
我们期待实现这样的效果:
拖动或点击按钮,开关向右滑动,变成
其中开关能随着手指的触摸滑动到相应位置,直到最后才固定在开关位置上
新建一个类继承自View,实现其两个构造方法
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()确定其形状。
但我们需要根据我们点击控件的不同行为来确定形状,这需要重写onTouchEvent()
其中的逻辑是:
当按下时,触发事件MotionEvent.Action_Down,若此时状态为关:
(1)若手指触摸点(通过event.getX()得到)在按钮的 中线右侧,按钮向右滑动一段距离(event.getX()与开关控件一半宽度之差,具体看下文源码)
(2)若手指触摸点在按钮中线左侧,按钮依旧处于最左(即“关”的状态)。
若此时状态为开:
(1)若手指触摸点在按钮中线左侧,按钮向左滑动一段距离
(2)若手指触摸点在按钮中线右侧,按钮依旧处于最右(即“开”的状态)
当滑动时,触发时间MotionEvent.Action_MOVE,逻辑与按下时一致
注意,onTouchEvent()需要设置返回值 为 Return true,否则无法响应滑动事件
当手指收起时,若开关中线位于整个控件中线左侧,设置状态为关,反之,设置为开。
具体源码如下所示:(还对外提供了一个暴露此时开关状态的接口)
自定义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;//默认状态
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("开关","开着的");
canvas.drawBitmap(mSlideButton,mSwitchButton.getWidth()-mSlideButton.getWidth(),0,mPaint);
}else{
Log.d("开关","关着的");
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, "关闭", 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>


猜你喜欢
- 专业的Android app开发人员会关注一些成熟的项目管理技术,以成功构建Android app,并让这个app在Google Play
- java多线程-同步块Java 同步块(synchronized block)用来标记方法或者代码块是同步的。Java 同步块用来避免竞争。
- 前言碎语今天博主分享一个Kubernetes集全管理软件,也就是Kubernetes web ui。是360团队开源的一款产品,Wayne
- 第一步:代码混淆(注意引入的第三方jar)在新版本的ADT创建项目时,混码的文件不再是proguard.cfg,而是project.prop
- 首先我们在项目中导入这个框架:implementation 'com.mcxiaoke.volley:library:1.0.19&
- ObjectUtils.isEmpty()和null区别分配内存和赋值的区别isEmpty():判断值是否为空,即使已经分配内存,但没有赋值
- 背景笔者使用 Spring Security 5.8 时,发现网上很多教程所教的 Spring Security 配置类 SecurityC
- 背景:今天新生成一个springboot项目,然而启动日志,还有mybatis的详细日志无法打印出来,自写程序中打印的日志可以输出;网上找了
- 上次写了一篇博文,但是每次更新图标时,桌面会闪烁(刷新)https://www.jb51.net/article/73350.htm,有博友
- SpringMVC在接收集合请求参数时,需要在Controller方法的集合参数里前添加@RequestBody,而@RequestBody
- 介绍微服务横行的互联网世界, 跨服务调用显得很平凡, 我们除了采用传统的http方式接口调用, 有没有更为优雅方便的方法呢?答案是肯定的,f
- 前言:Guarded Suspension意为保护暂停,其核心思想是仅当服务进程准备好时,才提供服务。设想一种场景,服务器可能会在很短时间内
- 前言一说到Socket,想必大家都或多或少有所涉及,从最初的计算机网络课程,讲述了tcp协议,而Socket就是对协议的进一步封装,使我们开
- 一、问题描述在接受 mq 消息的时候,需要做一个重试次数限制,如果超过 maxNum 就发邮件告警,不再重试。 所以我需
- 前言众所周知,在多个项目中可能会相同的模块,如果每个项目都去创建一遍的话,这样开发效率会很低。比如在开发一个APP应用的时候,有供APP使用
- Jmeter 执行Java 请求时,运行结束后报错,Tidying up remote @ Mon Feb 24 19:42:34 CST
- 本文主要是用到java中的swing技术,以及JMFjar中的API,为大家分享了java音乐播放器的具体实现代码,供大家参考,具体内容如下
- 一、Filter(过滤器)Filter接口定义在javax.servlet包中,是Servlet规范定义的,作用于Request/Respo
- 本文实例讲述了android中Bitmap用法。分享给大家供大家参考。具体如下:在Android SDK中可以支持的图片格式如下:png ,
- 在项目开发上,hibernate提供的经验简化了不少工作量和兼容性,但这些绝对需要有经验后才能明白,对于新手来说使用起来很困难。hibern