图文剖析自己定义View的绘制(以自己定义滑动button为例)
自己定义View一直是横在Android开发人员面前的一道坎。
一、View和ViewGroup的关系
从View和ViewGroup的关系来看。ViewGroup继承View。
View的子类。多是功能型的控件。提供绘制的样式,比方imageView,TextView等。而ViewGroup的子类,多用于管理控件的大小,位置。如LinearLayout,RelativeLayout等。从下图能够看出
从实际应用中看,他们又是组合关系,我们在布局中,经常是一个ViewGroup嵌套多个ViewGroup或View。而被嵌套的ViewGroup又会嵌套多个ViewGroup或View
例如以下
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
二、View的绘制流程
从View源代码来看,主要关系三个方法:
流程: 流程 measure --> layout --> draw
相应于我们要实现的方法是
实际绘制中。我们的思考顺序通常是这种:
-->onLayout
()
-->onDraw ()-->canvas的绘制
以下是我绘制的流程图:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
以下以自己定义滑动按钮为例,说明自己定义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>
图文剖析自己定义View的绘制(以自己定义滑动button为例)的更多相关文章
- 【Android自己定义View实战】之自己定义超简单SearchView搜索框
[Android自己定义View实战]之自己定义超简单SearchView搜索框 这篇文章是对之前文章的翻新,至于为什么我要又一次改动这篇文章?原因例如以下 1.有人举报我抄袭,原文链接:http:/ ...
- 自己定义View之绘制圆环
一.RingView 自己定义的view,构造器必须重写,至于重写哪个方法,參考例如以下: ①假设须要改变View绘制的图像,那么须要重写OnDraw方法.(这也是最经常使用的重写方式.) ②假设须要 ...
- 安卓自己定义View进阶-Canvas之绘制基本形状
Canvas之绘制基本形状 作者微博: @GcsSloop [本系列相关文章] 在上一篇自己定义View分类与流程中我们了解自己定义View相关的基本知识,只是,这些东西依然还是理论,并不能拿来(zh ...
- 自己定义 View 基础和原理
课程背景: 在 Android 提供的系统控件不能满足需求的情况下,往往须要自己开发自己定义 View 来满足需求,可是该怎样下手呢.本课程将带你进入自己定义 View 的开发过程,来了解它的一些原理 ...
- Android画图系列(二)——自己定义View绘制基本图形
这个系列主要是介绍下Android自己定义View和Android画图机制.自己能力有限.假设在介绍过程中有什么错误.欢迎指正 前言 在上一篇Android画图系列(一)--自己定义View基础中我们 ...
- Android View的绘制过程
首先是view的绘制过程~最主要的分三部分 measure layout draw 看字面意思,计算,布局,画~ android中控件相当于是画在一个无限大的画布上的,那就产生了几个问题 画布无限大, ...
- Android - View的绘制流程一(measure)
该博文所用的demo结构图: 相应的代码: MainActivity.java: [java] view plain copy <span style="font-family:Mic ...
- 自定义控件(View的绘制流程源码解析)
参考声明:这里的一些流程图援引自http://a.codekk.com/detail/Android/lightSky/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7% ...
- 【转】Android中View的绘制过程 onMeasure方法简述 附有自定义View例子
Android中View的绘制过程 当Activity获得焦点时,它将被要求绘制自己的布局,Android framework将会处理绘制过程,Activity只需提供它的布局的根节点. 绘制过程从布 ...
随机推荐
- 最长上升序列 LCS LIS
子序列问题 (一)一个序列中的最长上升子序列(LISLIS) n2做法 直接dp即可: ;i<=n;i++) { dp[i]=;//初始化 ;j<i;j++)//枚举i之前的每一个j ) ...
- flask 模板block super()的讲解
Flask强大的地方就可以引用模板,而且非常方便. 这里不得不介绍block这个概念. 模板的文件一般放在templates文件夹下,我们这里新建一个HTML文件,存放模板,'base.html' 在 ...
- C#窗体向另一个窗体实时传值及传值问题
C#窗体向另一个窗体实时传值 另外的传值方法:
- AGC015 C-Nuske vs Phantom Thnook AtCoder 思路 前缀和
目录 题目链接 题解 代码 题目链接 AGC015 C-Nuske vs Phantom Thnook AtCoder 题解 树的性质有: 如果每个蓝色连通块都是树,那么连通块个数=总点数−总边数. ...
- BZOJ.4653.[NOI2016]区间(线段树)
BZOJ4653 UOJ222 考虑二分.那么我们可以按区间长度从小到大枚举每个区间,对每个区间可以得到一个可用区间长度范围. 我们要求是否存在一个点被这些区间覆盖至少\(m\)次.这可以用线段树区间 ...
- LOJ.115.[模板]无源汇有上下界可行流(Dinic)
题目链接 参考:http://blog.csdn.net/clove_unique/article/details/54884437 http://blog.csdn.net/wu_tongtong/ ...
- 潭州课堂25班:Ph201805201 并发(进程,线程) 第十一课 (课堂笔记)
线程,进程,是实现并发的方法, 并行: 在同一时刻,同时运行多个任务,CPU 的数量大于等于任务数量, 并发: 在同一时间间隔内, 同时处理多个任务, 并行是并发. 进程:表示一个正在执行的程序, 操 ...
- 使用 P6Spy 来格式化 SQL 语句,支持 Hibernate 和 iBATIS
事情起因 在处理一个查询小功能的时候,自认为 SQL 语句和传参均正确,然而查询结果无匹配数据,在查看 Hibernate 自带 SQL 语句输出的时候带着问好感觉有点不爽,特别是想复制 SQL 语句 ...
- adblock 下载地址
addblock 的作用: 防止追踪.恶意域名,过滤横幅广告.弹窗广告以及视频广告. 用以支持网站的非侵入式广告将不会被屏蔽 下载地址:https://pan.baidu.com/share/li ...
- linux和CentOS下网卡启动、配置等ifcfg-eth0教程(转自)
转自:http://www.itokit.com/2012/0415/73593.html it 动力总结系统安装好后,通过以下二个步骤就可以让你的系统正常上网(大多正常情况下).步骤1.配置/etc ...