自定义view

  • 定义

    View是所有控件的基类:Button、Textview View是一种界面层空间的抽象,VIew与ViewGroup代表了控件和控件组 这意味着View本身可以是单个控件也可以是由多个控件组成的一个控件组,这种结构就形成了一种View树的结构 类似于前端的DOM树

  • View的层级结构

Object-View-TextView-TestButton

  • View的位置参数

VIew的位置主要是由四个顶点的位置来决定的:top(左上角纵坐标)、left(左上角横坐标)、right(右下角横坐标)、bottom(右下角纵坐标)(这都是一种相对坐标)

VIew的宽高和坐标关系:

  • width=right-left height=bottom-top

利用Left=getLeft(); Right=getRight(); Top=getTop; Bottom=getBottom().

  • 从Android3.0开始,View新增了X,Y,translationXhetranslationY

XY是左上角的图标 translationX和TranslationY是对于父容器的偏移量,默认值为0

View也提供了set和get方法给这两个参数

换算关系如下

X=left+translationX

y=top+translationY

View在平移的过程中 top和left是表示原始左上角的位置信息,值并不会发生改变

MotionEvent和TouchSlop

  • MotionEvent

在手指接触屏幕所产生的一系列事件中,典型的事件类型有如下几种

ACTION_DOWN:手指刚接触屏幕

ACTION_MOVE:手指在屏幕上移动;

ACTION_UP:手机从屏幕上松开的一瞬间

一般会有这两种情况:

  • 手指点击屏幕后再松开:DOWN->UP
  • 手指点击屏幕滑动一会后松开:DOWN->MOVE->….->MOVE->UP

通过MotionEvent对象来获得点击事件发生的X和Y坐标。

getX/getY/getRawX/getRawY

getX和getY是返回当前View左上角的X和Y坐标。而getRawX和getRawY是返回手机屏幕最上角的X和Y坐标

  • TouchSlop

定义:系统所能识别出的被认为是滑动的最小距离

当滑动的距离小于这个值的时候,系统就不任务进行了一次滑动。所以说TouchSlop是一个常量

我们通过ViewConfiguration.get(getComtext()).getScaledTouchSlop()来获得

VelocityTracker、GestureDestector和Scroller

  • VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度

方法:VIew的onTouchEvent方法VelocityTracker.obtain();来追踪事件的速度

此时我们需要去添加事件从而进行追踪当前事件的速度:velocityTracker.addMovement(event);//添加这个事件

我们再先计算速度,再去获取速度(都是这个流程)

计算速度:velocityTracker.computeCurrentVelocity(1000)//在1000ms中经过的像素数 从左往右为正,从右往左为负

然后再去get

最后当我们不需要VelocityTracker这个对象的时候去:

velocityTracker.clear();

velocityTracker.recycle();

  • GestureDetector:手势检测,辅助检测用户的单机、滑动、长按、双击

首先创建一个GestureDetector对象,这个对象实现接口OnGetureListener,还可以去实现OnDoubleTapListener监听双击行为

1
2
GestureDetector mgestureDetector =new GestureDetector(this)
mgestureDetector.setIsLongpressEnabled(false)//解决长按屏幕无法拖动的现象

在待监听的View的onTouchEvent方法中添加

1
2
boolean consume=mGestureDetector.onTouchEvent(event);
return consume;

至此,就可以去实现监听和接口的方法了

  • Scroller:弹性滑动对象,实现View的弹性滑动

具体3.2中讲解

View的滑动(第一种View的滑动效果实现)

  • scrollTo/scrollBy:View专门实现滑动的功能所提供的方法。

scrollTo是一种绝对滑动 scrollBy是一种在To的基础上的相对滑动

如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。

动画的使用(第二种实现View的滑动效果)

  • 定义:使用动画来移动View,而不是平移(滑动)来移动View
  • 分类
    • 传统动画:
    • 属性动画:采用开源库
  • 注意:VIew的动画只是对View的一个影像做的操作,没有真正的改变View的位置参数,宽与高
  • 具体在第五章《Android开发艺术探索》讲解

改变布局参数(第三中实现View的滑动效果)

通过改变布局参数从而来实现VIew的滑动

两种办法:

  • 设置一个空的View来“挤”需要滑动的组件(1)
  • 重新设置滑动组件的marginLeft参数(2)

(2)来实现

1
2
3
4
MarginLayoutParams params=(MarginLayoutParams)mButton1.getLayoutParams();
params.width+=100;//参数变化
params.leftMargin+=100;//参数变化
mButton1.requestLayout();//重新排布局,刷新布局,这样就达到了滑动的效果

滑动方式的对比

  • scrollTo/scrollBy:View的原生方法,专门用于View的滑动,方便,不影响点击事件,但是只能滑动View本身不能滑动VIew本身
  • 动画:采用属性动画的办法是一种比较好的办法,几乎没有缺点。复杂效果必须要通过动画来实现,如果无交互需求的话可以采用动画,操作简单
  • 改变布局:使用麻烦,主要适用有交互性的View

弹性滑动

  • 思想:将一次大的滑动分成若干次小的滑动并在一段时间内完成,

方法一:使用Scroller

当我们new了一个新的Scroller对象时,我们调用startScrooller方法时,我们只是保存了几个我们传递的参数

,这表示我们并不是利用Scroller的start来实现一种滑动的。我们调用invalidate方法,证明:invalidate方法导致view重绘,在View的draw方法中又回去调用computeScroll方法,computeScroll方法在View中是一个空实现

所以需要自己去实现,应为这个方法才会去实现弹性的滑动

在computerScroll方法中,返回值是一个布尔,如果返回的是对的,证明这个View滑动还没有结束。反之结束

方法二:通过动画

使用ObjectAnimator.ofFloat(yargetView,“translationx”,0,100)

(100).start;

可以让一个View内容在100ms内向左移动100像素

方法三:通过延迟策略

采用Handler或者View的机制

View的事件分发机制

  • 点击事件的传递规则

MotionEvent

点击事件的传递过程是由三个组成dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent

dispatchTouchEvent(MotionEvent ev)表示传递给当前View的onTouchEvent和下级的dispatchTouchEvent方法的影响 布尔类型

表示是否消耗当前事件

onInterceptTouchEvent(MotionEvent event)在方法中内部调用,用于判断是否拦截某个事件,如果View拦截了某个事件,那么在同一个事件序列当中,此方法不再去调用,返回结果表示是否拦截当前事件 布尔类型

onTouchEvent方法在DispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件 布尔类型 如果不消耗,则在同一个事件序列中,当前View无法在接收到事件

“对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。”

摘录来自: 任玉刚. “Android开发艺术探索。” Apple Books.

上述我们说明了如何去拦截,下面我们来说说怎么处理

当一个View设置了OnTouchListener

在OnTouchListener中的onTouch方法被回调,然后这个方法的返回值是false则调用,如果是true则不会调用

优先级排列:OnTouchListener>OnTouchEvent>OnClickListener

  • 点击事件的传递顺序:Activity->Window->View

当View接受到事件后,就会按照事件分发机制去处理

但如果OnTouchEvent返回的结果是false则会往上传递(父类的OnTouchEvent)最后如果解决不了上传至Activity的OnTouchEvent

  • 整个传递机制
  1. 以Down开始,中间无数的Move,以Up结束
  2. 一个事件序列只能被一个View拦截且消耗
  3. 某个View一旦决定拦截,那么这个事件序列都只能由他来处理
  4. 如果某个View一旦开始处理事件,如果没有消耗ACTION_DOWN事件,就同一时间序列的其他事件都不会再交于他来处理

事件分发的源码解析

操作流程

  1. 当点击事件MotionEvent发生操作的时候,

  2. 我们将事件传递给当前的Activity,

  3. 再由Activity的DispatchTouchEvent来进行事件派发。

  4. Activity的内部Window来完成,会将事件传递给Decorview

  5. decorview一般是当前的底层容器(setContentView的参数)
    通过

    1
    Activity.getWindow.getDecorView()

    获得

    源码解析

  6. Activity的dispatchTouchEvent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public boolean dispatchTouchEvent(MotionEvent ev){
    if(ev.getAction()==MotionEvent.ACTION_DOWN){
    onUserInteraction();
    }
    if(getWindow().superDispatchTouchEvent(ev)){
    return true;
    }
    return onTouchEvent(ev);
    }

    返回true证明有人处理,那就循环结束
    如果返回false,证明没人处理,就调用Activity的onTouchEvent

  7. Window->ViewGroup
    首先明确Window是一个抽象类,Window的superDispatchTouchEvent(MotionEvent event)也是一个抽象的方法
    所以先要找到实现类
    Window的实现类是PhoneWindow
    所以我们查看PhoneWindowsuperDisptachTouchEvent方法

    1
    2
    3
    public boolean superDispatchTouchEvent(MotionEvent event){
    return mdecor.superDispatchTouchEvent(event);
    }

    PhoneWindow直接将事件传递给了DecorView

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private final class DecorView extends FrameLayout implements RootViewSur-faceTaker
    private DecorView mdecor;
    @override
    public final View getDecorView(){
    if(mDecor==null){
    installDecor();
    }
    return mDecor;
    }

    在此时已经传递到了顶级的View
    也就是ViewGroup,根View,通过传递给顶级的View来进行一个处理(就是setContentView所设置的View)

  8. 顶级View对事件的分发机制
    在View中的时间处理如下:
    如果顶级View的拦截事件onInterceptTouchEvent返回true,则事件由ViewGroup来处理,如果ViewGroup的mOntouchListener被设置,onTouch会调用,否则是onTouchEvent被调用,如果都提供的话,onTouch会屏幕onTouchEvent,在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用,如果ViewGroup不拦截事件,则会传递到子View上
    子View的dispatchTouchEvent会被调用。接下来都一样