Android自定义控件总结
通过观察superDispatchTouchEvent()方法的调用链,我们可以发现事件的传递顺序:
- PhoneWinodw.superDispatchTouchEvent()
- DecorView.dispatchTouchEvent(event)
- ViewGroup.dispatchTouchEvent(event)
事件一层层传递到了ViewGroup里。
- 外部处理,重写父view的onInterceptTouchEvent ,MotionEvent的事件全部返回false,不拦截;
- 内部处理。重写子view的dispatchTouchEvent,通过requestDisallowInterceptTouchEvent方法(这个方法可以在子元素中干预父元素的事件分发过程),请求父控件不拦截自己的事件,true是不拦截,false是拦截。
- 在Activity中执行setContentView方法后会执行PhoneWindow的setContentView,在该方法中会生成DecorView 组件作为应用窗口的顶层视图。
- DecorView 是PhoneWindow的内部类,继承至FrameLayout,DecorView 会添加一个id为content的FrameLayout作为根布局,Activity的xml文件会通过LayoutInflater的inflate方法解析成View树添加到id为content的FrameLayout中。
- ViewRoot不是View,它的实现类是ViewRootImpl,ViewRoot是DecorView的“管理者”。它是DecorView和WindowManager之间的纽带。
- 毕竟“管理者”,所以View的绘制流程是从ViewRoot的performTraversals方法开始的。所以performTraversals方法依次调用performMeasure,performLayout和performDraw三个方法。然后各自经历measure、layout、draw三个流程最终显示在用户面前,用户在点击屏幕时,点击事件随着Activity传入Window,最终由ViewGroup/View进行分发处理。
- 对于顶级View(DecorView)其MeasureSpec由窗口的尺寸和自身的LayoutParams共同确定的。
- 对于普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同确定的。
//如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值可以为0
//如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //这里已经帮我们测好了ImageView的规则下的宽高,并且通过了setMeasuredDimension方法赋值进去了。
super.onMeasure(widthMeasureSpec, heightMeasureSpec); //我们这里通过getMeasuredWidth/Height放来获取已经赋值过的测量的宽和高
//然后在ImageView帮我们测量好的宽高中,取小的值作为正方形的边。
//然后重新调用setMeasuredDimension赋值进去覆盖ImageView的赋值。
//我们从头到位都没有进行复杂测量的操作,全靠ImageView。哈哈
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
- setMeasuredDimension后才能getmeasure宽高,super里做了这步,因为这方法是用来设置view测量的宽和高。
- 如果需要重新测量或者动态改变自定义控件大小那就需要根据自己需求重写规则makeMeasureSpec,简单说就是规则改变了就需要重写规则。
- 重写onMeasure方法的目的是为了能够给view一个warp_content属性下的默认大小,因为不重写onMeasure,那么系统就不知道该使用默认多大的尺寸。如果不处理,那wrap_content就相当于match_parent。所以自定义控件需要支持warp_content属性就重写onMeasure。那如何重写呢?
- 可以自己尝试一下自定义一个View,然后不重写onMeasure()方法,你会发现只有设置match_parent和wrap_content效果是一样的,事实上TextView、ImageView 等系统组件都在wrap_content上有自己的处理。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec); //指定一组默认宽高,至于具体的值是多少,这就要看你希望在wrap_cotent模式下
//控件的大小应该设置多大了
int mWidth = 200;
int mHeight = 200; int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i); // 取得下标为I的子view /**
* 父view 会根据子view的需求,和自身的情况,来综合确定子view的位置,(确定他的大小)
*/
//指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置
view.layout(0+i*getWidth(), 0, getWidth()+i*getWidth(), getHeight()); }
}
- ViewGroup需要控制子view如何摆放的时候需要实现onLayout。
- View没有子view,所以不需要onLayout方法,需要的话实现onDraw
- 继承系统已有控件或容器,比如FrameLayou,它会帮我们去实现onMeasure方法中,不需要去实现onMeasure, 如果继承View或者ViewGroup的话需要warp_content属性的话需要实现onMeasure方法
- 自定义ViewGroup大多时候是控制子view如何摆放,并且做相应的变化(滑动页面、切换页面等)。自定义view主要是通过onDraw画出一些形状,然后通过触摸事件去决定如何变化
- getMeasuredHeight(): 控件实际的大小
- getHeight():控件显示的大小,必须在onLayout方法执行完后,才能获得宽高,这种方法不好,得等所以的都测量完才能获得。获取到的是屏幕上显示的高度,getMeasuredHeight是实际高度。
view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
headerView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int headerViewHeight = headerView.getHeight();
//直接可以获取宽高
}
});
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 当尺寸有变化的时候调用
mHeight = getMeasuredHeight();
mWidth = getMeasuredWidth();
// 移动的范围
mRange = (int) (mWidth * 0.6f); }
protected void onFinishInflate() {
super.onFinishInflate();
// 容错性检查 (至少有俩子View, 子View必须是ViewGroup的子类)
if(getChildCount() < 2){
throw new IllegalStateException("布局至少有俩孩子. Your ViewGroup must have 2 children at least.");
}
if(!(getChildAt(0) instanceof ViewGroup && getChildAt(1) instanceof ViewGroup)){
throw new IllegalArgumentException("子View必须是ViewGroup的子类. Your children must be an instance of ViewGroup");
} mLeftContent = (ViewGroup) getChildAt(0);
mMainContent = (ViewGroup) getChildAt(1);
}
- view的位置参数有left、right、top、bottom(可以getXX获得),3.0后又增加了几个参数:x、y、translationX和translationY,其中x和y是view左上角的坐标,而translationX和translationY是view左上角相对于父容器的偏移量。这些参数都是相对于父容器的坐标,并且translationX和translationY的默认值是0,他们的换算关系是:x=left+translationX y=top+ translationY。需要注意的是,view在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数
- touchslop是系统所能识别出的被认为是滑动的最小距离,比如当俩次滑动事件的滑动距离小于这个值,我们就可以认为未达到滑动距离的临界值
追溯到View的dispatchTouchEvent源码查看,有这么一段代码
public boolean dispatchTouchEvent(MotionEvent event) {
if (!onFilterTouchEventForSecurity(event)) {
return false;
} if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
}
当以下三个条件任意一个不成立时,
- mOnTouchListener不为null
- view是enable的状态
- mOnTouchListener.onTouch(this, event)返回true,
函数会执行到onTouchEvent。在这里我们可以看到,首先执行的是mOnTouchListener.onTouch的方法,然后是onTouchEvent方法
继续追溯源码,到onTouchEvent()观察,发现在处理ACTION_UP事件里有这么一段代码
if (!post(mPerformClick)) { performClick(); }
此时可知,onClick方法也在最后得到了执行
- setOnTouchListener() 的onTouch
- onTouchEvent()
- onClick()
1)dispatchTouchEvent:这个方法用来分发事件,如果拦截了交给ontouchevent处理,对应上面的和ontounch理解,否则传给子view
2)onInterceptTouchEvent: 这个方法用来拦截事件,返回true表示拦截(不允许事件继续向子view传递),false不拦截,如果自定义viewgroup里某个子view需要自己处理事件,就需要重写改方法,让他返回false。
3)onTouchEvent: 这个方法用来处理事件。Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。,子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。
2.move的时候计算偏移量,并用scrollTo()或scrollBy()方法移动view。这俩个方法都是快速滑动,是瞬间移动的。
注意:滚动的并不是viewgroup内容本身,而是它的矩形边框。
三种滑动的方法
- 使用scrollTo()或scrollBy()
- 动画
- 实时改变layoutparams,重新布局
* @param startX 开始时的X坐标
* @param startY 开始时的Y坐标
* @param disX X方向 要移动的距离
* @param disY Y方向 要移动的距离
myScroller.startScroll(getScrollX(),0,distance,0,Math.abs(distance));//持续的时间
/**
* Scroller不主动去调用这个方法
* 而invalidate()可以掉这个方法
* invalidate->draw->computeScroll
*/
@Override
public void computeScroll() {
super.computeScroll();
if(scroller.computeScrollOffset()){//返回true,表示动画没结束
scrollTo(scroller.getCurrX(), 0);
invalidate();
}
}
mDectector.onTouchEvent(event);// 委托手势识别器处理触摸事件
...
case MotionEvent.ACTION_UP: if(!isFling){// 在没有发生快速滑动的时候,才执行按位置判断currid
int nextId = 0;
if(event.getX()-firstX>getWidth()/2){ // 手指向右滑动,超过屏幕的1/2 当前的currid - 1
nextId = currId-1;
}else if(firstX - event.getX()>getWidth()/2){ // 手指向左滑动,超过屏幕的1/2 当前的currid + 1
nextId = currId+1;
}else{
nextId = currId;
}
moveToDest(nextId);
// scrollTo(0, 0);
} isFling = false; break;
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
//移动屏幕
/**
* 移动当前view内容 移动一段距离
* disX X方向移的距离 为正是,图片向左移动,为负时,图片向右移动
* disY Y方向移动的距离
*/
scrollBy((int) distanceX, 0); return false;
} @Override
/**
* 发生快速滑动时的回调
*/
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
isFling = true;
if(velocityX>0 && currId>0){ // 快速向右滑动。当前子view的下标
currId--;
}else if(velocityX<0 && currId<getChildCount()-1){ // 快速向左滑动
currId++;
} moveToDest(currId); return false;
} @Override
public boolean onDown(MotionEvent e) {
return false;
}
});
// a.初始化 (通过静态方法)
mDragHelper = ViewDragHelper.create(this , mCallback); // b.传递触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 传递给mDragHelper
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
mDragHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
// 返回true, 持续接受事件
return true;
}
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
// c. 重写事件 // 1. 根据返回结果决定当前child是否可以拖拽
// child 当前被拖拽的View
// pointerId 区分多点触摸的id
@Override
public boolean tryCaptureView(View child, int pointerId) {
Log.d(TAG, "tryCaptureView: " + child);
return true;
}; @Override
public void onViewCaptured(View capturedChild, int activePointerId) {
Log.d(TAG, "onViewCaptured: " + capturedChild);
// 当capturedChild被捕获时,调用.
super.onViewCaptured(capturedChild, activePointerId);
} @Override
public int getViewHorizontalDragRange(View child) {
// 返回拖拽的范围, 不对拖拽进行真正的限制. 仅仅决定了动画执行速度
return mRange;
} // 2. 根据建议值 修正将要移动到的(横向)位置 (重要)
// 此时没有发生真正的移动
public int clampViewPositionHorizontal(View child, int left, int dx) {
// child: 当前拖拽的View
// left 新的位置的建议值, dx 位置变化量
// left = oldLeft + dx;
Log.d(TAG, "clampViewPositionHorizontal: "
+ "oldLeft: " + child.getLeft() + " dx: " + dx + " left: " +left); if(child == mMainContent){
left = fixLeft(left);
}
return left;
} // 3. 当View位置改变的时候, 处理要做的事情 (更新状态, 伴随动画, 重绘界面)
// 此时,View已经发生了位置的改变
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
// changedView 改变位置的View
// left 新的左边值
// dx 水平方向变化量
super.onViewPositionChanged(changedView, left, top, dx, dy);
Log.d(TAG, "onViewPositionChanged: " + "left: " + left + " dx: " + dx); int newLeft = left;
if(changedView == mLeftContent){
// 把当前变化量传递给mMainContent
newLeft = mMainContent.getLeft() + dx;
} // 进行修正
newLeft = fixLeft(newLeft); if(changedView == mLeftContent) {
// 当左面板移动之后, 再强制放回去.
mLeftContent.layout(0, 0, 0 + mWidth, 0 + mHeight);
mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
}
// 更新状态,执行动画
dispatchDragEvent(newLeft); // 为了兼容低版本, 每次修改值之后, 进行重绘
invalidate();
} // 4. 当View被释放的时候, 处理的事情(执行动画)
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// View releasedChild 被释放的子View
// float xvel 水平方向的速度, 向右为+
// float yvel 竖直方向的速度, 向下为+
Log.d(TAG, "onViewReleased: " + "xvel: " + xvel + " yvel: " + yvel);
super.onViewReleased(releasedChild, xvel, yvel); // 判断执行 关闭/开启
// 先考虑所有开启的情况,剩下的就都是关闭的情况
if(xvel == 0 && mMainContent.getLeft() > mRange / 2.0f){
open();
}else if (xvel > 0) {
open();
}else {
close();
} } @Override
public void onViewDragStateChanged(int state) {
// TODO Auto-generated method stub
super.onViewDragStateChanged(state);
}
/**
* 根据范围修正左边值
* @param left
* @return
*/
private int fixLeft(int left) {
if(left < 0){
return 0;
}else if (left > mRange) {
return mRange;
}
return left;
}
private void animViews(float percent) {
// > 1. 左面板: 缩放动画, 平移动画, 透明度动画
// 缩放动画 0.0 -> 1.0 >>> 0.5f -> 1.0f >>> 0.5f * percent + 0.5f
// mLeftContent.setScaleX(0.5f + 0.5f * percent);
// mLeftContent.setScaleY(0.5f + 0.5f * percent);
ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftContent, 0.5f + 0.5f * percent);
// 平移动画: -mWidth / 2.0f -> 0.0f
ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth / 2.0f, 0));
// 透明度: 0.5 -> 1.0f
ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.5f, 1.0f)); // > 2. 主面板: 缩放动画
// 1.0f -> 0.8f
ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f)); // > 3. 背景动画: 亮度变化 (颜色变化)
getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK, Color.TRANSPARENT), Mode.SRC_OVER);
Android自定义控件总结的更多相关文章
- Android自定义控件之自定义ViewGroup实现标签云
前言: 前面几篇讲了自定义控件绘制原理Android自定义控件之基本原理(一),自定义属性Android自定义控件之自定义属性(二),自定义组合控件Android自定义控件之自定义组合控件(三),常言 ...
- Android自定义控件之自定义组合控件
前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发 ...
- Android自定义控件之自定义属性
前言: 上篇介绍了自定义控件的基本要求以及绘制的基本原理,本篇文章主要介绍如何给自定义控件自定义一些属性.本篇文章将继续以上篇文章自定义圆形百分比为例进行讲解.有关原理知识请参考Android自定义控 ...
- Android自定义控件之基本原理
前言: 在日常的Android开发中会经常和控件打交道,有时Android提供的控件未必能满足业务的需求,这个时候就需要我们实现自定义一些控件,今天先大致了解一下自定义控件的要求和实现的基本原理. 自 ...
- Android自定义控件1
概述 Android已经为我们提供了大量的View供我们使用,但是可能有时候这些组件不能满足我们的需求,这时候就需要自定义控件了.自定义控件对于初学者总是感觉是一种复杂的技术.因为里面涉及到的知识点会 ...
- 一起来学习Android自定义控件1
概述 Android已经为我们提供了大量的View供我们使用,但是可能有时候这些组件不能满足我们的需求,这时候就需要自定义控件了.自定义控件对于初学者总是感觉是一种复杂的技术.因为里面涉及到的知识点会 ...
- [Xamarin.Android] 自定义控件
[Xamarin.Android] 自定义控件 前言 软件项目开发的过程中,免不了遇到一些无法使用内建控件就能满足的客户需求,例如:时速表.折线图...等等.这时开发人员可以透过自定义控件的方式,为项 ...
- android自定义控件实现TextView按下后字体颜色改变
今天跟大家分享一下Android自定义控件入门,先介绍一个简单的效果TextView,按下改变字体颜色,后期慢慢扩展更强大的功能 直接看图片 第一张是按下后截的图,功能很简单, ...
- android 自定义控件(初篇)
android 自定义控件 在写UI当中很多时候会用到自定义的控件,其实自定义控件就像是定义一个类进行调用就OK了.有些相关的感念可以查看API 下面就用个简单的例子来说明自定义控件: public ...
- Android自定义控件:进度条的四种实现方式(Progress Wheel的解析)
最近一直在学习自定义控件,搜了许多大牛们Blog里分享的小教程,也上GitHub找了一些类似的控件进行学习.发现读起来都不太好懂,就想写这么一篇东西作为学习笔记吧. 一.控件介绍: 进度条在App中非 ...
随机推荐
- selemium 常用查找方法
1.selemium.FindElement(By.LinkText("下一步")) 2.selemium.FindElement(By.Id("userNumber&q ...
- jQuery.validate.js 自定义错误信息
var validate = $("form").validate({....})validate.showError({"username":"us ...
- Skipping acquire of configured file ···doesn't support architecture 'i386' acquire of configured file
系统更新的时候报错: Skipping acquire of configured file 'main/binary-i386/Packages' as repository 'http://rep ...
- P2024 食物链
题面:P2024 食物链 emmm其实不太难想 开三倍的数组 1~n:是当前动物的同类 n~2*n:是当前动物吃的动物 2*n~3*n:是吃当前动物的动物 emmmm #include<iost ...
- sort 对多列进行排序
sort -t '\t' -k 3,3 -k 2,2 文件名 # 先对第三列进行排序,然后再对第二列进行排序
- Windows操作系统发展历程
1964年贝尔实验室(Bell).麻省理工学院(MIT)及美国通用电气公司(GE)为了开发出一套安装在大型主机上多人多工的操作系统开发了Multics系统.Multics是一个全面的,通用编程系统.后 ...
- 解决拼音输入法会触发input事件的问题
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script sr ...
- pl/sql调试存储过程
1.找到对应的存储过程 2.在存储过程名称上右键,选择Test 3.点击1标识的按钮(begin debugger),选择2开始调试 4.存储过程如需参数,需要在右侧下方的表格区域(3)填入对应的值即 ...
- 【转】async & await 的前世今生(Updated)
async 和 await 出现在C# 5.0之后,给并行编程带来了不少的方便,特别是当在MVC中的Action也变成async之后,有点开始什么都是async的味道了.但是这也给我们编程埋下了一些隐 ...
- SQL视图命名规则:一般以V_xxx_xxxxxx