Android开发艺术View基础

一、什么是View

View是Android中所有控件的基类,不管是常见的Button和TextView还是相对复杂的LinearLayout和ListView。除了View,还有ViewGroup,可被翻译为控件组,ViewGroup内部可以包含许多控件,在Android设计中,ViewGroup也继承了View,所以View本身就可以是一个单个的控件也可以是多个控件组成的一组控件。根据这个概念可以知道Button是一个View,而LinearLayout不但是一个View还是一个ViewGroup,ViewGroup内部可以有多个子View或ViewGroup。
了解View的层次关系可以更好地理解View的工作原理。下图可以看到Button是一个View,它继承TextView,而TextView直接继承View,不敢这样Button都是一个View。

图1  Button的层次结构

二、View的位置参数

View的位置主要有四个顶点来决定,分别对应于View的四个属性:top、left、right、bottom。其中top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标。注意这些坐标都是相对于View的父容器来说的,它是一种相对坐标。View的坐标和父容器的关系如图2所示。在Android中,x轴的正方向是水平向右,y轴的正方向是垂直向下。这里要和数学坐标系区分开来,就是y轴正方向不一样。

注意:View的坐标系统是相对于父控件而言的。

图2 View的位置坐标和父容器的关系

getTop()        //获取子View左上角距父View顶部的距离
getLeft()       //获取子View左上角距父View左侧的距离
getBottom()     //获取子View右下角距父View顶部的距离
getRight()      //获取子View右下角距父View左侧的距离

根据上面的坐标图可以很容易得出View的宽高和坐标的关系:

width = right - left
height = bottom - top

从Android3.0开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量,默认值为0,这几个参数也是相对于父容器的坐标。这几个参数的换算关系如下:

x = left + translationX
y = top + translationY

需要注意的是,View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生变化,此时发生变化的是x、y、translationX、translationY这四个参数。

三、MotionEvent和TouchSlop

1.MotionEvent

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

  • ACTION_DOWN—–手指刚接触屏幕
  • ACTION_MOVE——手指在屏幕上移动
  • ACTION_UP———–手指在屏幕上松开的一瞬间。

正常情况下一次手指触摸事件行为会触发一系列点击事件,有如下情况:

  • 点击屏幕后松开,事件序列为:DOWN -> UP
  • 点击屏幕滑动一会在松开,事件序列为:DOWN -> MOVE -> …->MOVE -> UP

对于上述的事件序列,通过MotionEvent对象可以得到点击事件发生的 x 和 y 坐标。为此,系统提供的 getX/getY 方法,返回的是性对于当前 View 左上角的 x 和 y 坐标,提供的 getRawX/getRawY 方法返回的是相对于手机屏幕左上角的 x 和 y 坐标。MotionEvent中 get 和 getRaw 的区别如下:
get和getRaw的区别

2.TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,也就是说当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,系统就不认为在进行滑动操作。
这是一个常量,和设备有关,不同设备上这个值可能不同。可以通过如下公式获取这个常量:

ViewConfiguration.get(getContext()).getScaledTouchSlop()

当在处理滑动时,可以通过这个常量来做一些过滤,比如当两次滑动事件的滑动距离小于这个值,就可以认为它们不是滑动。定义这个常量可以在
frameworks/base/core/res/res/values/config.xml文件中找到,对这个常量的定义源码表示如下:

<!-- Base "touch slop" value used by ViewConfiguration as a
         movement threshold where scrolling should begin. -->
    <dimen name="config_viewConfigurationTouchSlop">8dp</dimen>

四、VelocityTracker、Scroller和GestureDetector

1.VelocityTracker

速度追踪,触摸事件速度的追踪助手,用于手指在滑动过程中的速度追踪,包括水平和垂直方向的速度。使用时首先在View的onTouchEvent中追踪当前单击事件的速度:

// 通过obtain方法取得VelocityTracker类的对象
VelocityTracker velocityTracker = VelocityTracker.obtain();
// 使用addMovement(MotionEvent)将收到的触摸事件放在其中
velocityTracker.addMovement(event); 

然后采用如下方式获取当前的滑动速度:

velocityTracker.computeCurrentVelocity(1000); // 计算速度
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

在这一步中有两点需要注意:

  • 获取速度前必须先计算速度,即必须在getXVelocity和getYVelocity两个方法前调用computeCurrentVelocity方法。
  • 这里所说的速度是指在一段时间内手指所滑过的像素数,如将时间间隔设为1000ms时,即在1s内,手指在水平方向从左向右滑过100像素,那么水平速度就是100。如果手指从右向左滑动,水平方向速度为负,正负值理解可以结合Android坐标系。速度计算公式:
               速度 = (终点位置 - 起始位置) / 时间段

注意:computerCurrentVelocity方法的参数是表示时间间隔,单位是毫秒(ms),计算得到的速度就是在这段时间内手指在水平或垂直方向上划过的像素数。在上面的例子中是在1000ms时间内划过100像素,那如果通过
velocityTracker.computerCurrentVelocity(100)来获取速度,得到的速度就是手指在100ms内划过的像素数,因此水平速度就成了10像素/每100ms(滑动过程是匀速情况下),即水平速度为10。
最后,在不需要使用时,要调用clear方法来重置并回收:

velocityTracker.clear();
velocityTracker.recycler();
2.Scroller

弹性滑动对象,用于实现View的弹性滑动。我们使用View的scrollTo/scrollBy方法来进行滑动是瞬间完成的,没有过渡效果,这个时候就可以通过Scroller来实现有过渡效果的滑动。Scroller本身不能让View弹性滑动,它需要和View的computeScroll方法配合才能完成。分析一下源码来探究为什么能实现弹性滑动。如何使用Scroller有固定的典型代码:

Scroller mScroller = new Scroller(mContent);

// 缓慢滑动到指定位置
private void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    // 1000ms内滑动向destX,效果就是慢慢滑动
    mScroller.startScroll(scrollX, 0, delta, 0, 1000);
    invalidate();
}

@Override
public void computeScroll() {
    // 使用computeScrollOffset()跟踪x/y坐标的更改位置。该方法返回一个布尔值,以指示滚动程序是否完成。
    // 可以使用此getCurrX,getCurrY方法来查找x和y坐标的当前偏移量
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}

描述一下上面代码工作原理:当构造一个Scroller对象并且调用它的startScroll方法时,Scroller内部什么也没有做,只是保存了我们传递的几个参数,看startScroll源码可以看出:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

调用startScroll并不能让View滑动,但在上面典型代码段中startScroll下面的invalidate方法会让View弹性滑动。invalidate方法会导致View重绘,在View的draw方法中又会去调用computeScroll方法。computeScroll方法在View中是空实现,需要我们自己去实现这个方法,正是这个方法才让View实现弹性滑动。

原因:在上面典型代码段中,当View重绘后会在draw方法中调用computeScroll方法,而computeScroll又会去向Scroller获取当前的scrollX和scrollY,然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法进行第二次重绘,重绘过程和第一次一样,还是会导致computeScroll方法调用,然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo方法滑动到新的位置,这样反复,直到整个滑动过程结束。

看一下Scroller的computeScrollOffset方法:

/**
     * Call this when you want to know the new location.  If it returns true,
     * the animation is not yet finished.
     */ 
    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;

                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);

                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

这个方法会根据时间的流逝来计算出当前的scrollX和scrollY的值。

3.GestureDetector

使用提供的MotionEvents检测各种手势和事件,检测用户的单击、滑动、长按、双击等。 GestureDetector.OnGestureListener回调将在特定运动事件发生时通知用户。此类只能用于通过触摸报告的MotionEvents(不要用于轨迹球事件)。
使用时首先需要为View创建一个GestureDetector对象并实现OnGestureListener接口,根据需要还可以实现OnDoubleTapListener监听双击行为:

/// 传入上下文对象和使用提供的侦听器创建GestureDetector对象
GestureDetector mGestureDetector = new GestureDetector(Context context, GestureDetector.OnGestureListener listener);

// 解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);

然后,接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加如下实现:

boolen consume = mGestureDetector.onTouchEvent(event);
return consume;

最后就可以有选择地实现OnGestureListener和OnDoubleTapListener中的方法了。

下面是OnGestureListener接口方法:

OnGestureListener接口方法

下图是OnDoubleTapListener接口方法:

OnDoubleTapListener接口方法

但我们在实际开发中可以不使用GestureDetector,可以自己在View的onTouchEvent中实现所需的监听。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值