首页 热点资讯 义务教育 高等教育 出国留学 考研考公
您的当前位置:首页正文

Android View相关(一)View的参数与滑动实现

2024-12-21 来源:化拓教育网

主要总结了:

  1. View的基础知识:
    • View的mTop、mLeft、mRight、mBottom四个参数和对应的四个get()。
    • View的getTanslationX() getTranslationY()、getX() getY()。
    • MotionEvent的典型事件和getX()、getY()、getRawX()、getRawY()。
    • TouchSlop最小滑动距离。
    • Velocity Tracker滑动速度。
    • GestureDetector和它的回调接口OnGestureListener、OnDoubleTapListener。
  2. View的滑动:
    • scrollTo()、scrollBy()的使用和实现,mScrollX、mScrollY参数。
    • View动画和属性动画实现滑动。
    • 改变参数布局实现滑动。
  3. View的弹性滑动:
    • Scroller实现弹性动画和原理。
    • 利用动画特性实现弹性动画。
    • 其他方法实现弹性动画。

View的基础知识

View的位置参数

mTop mLeft mRight mBottom

View的位置主要通过它的四个顶点来决定,对应View的四个属性。

  • mTop 左上角纵坐标
  • mLeft 左上角横坐标
  • mRight 右下角横坐标
  • mBottom 右下角纵坐标

这四个参数指的是View的原始位置信息,平移并不会改变这四个参数的值。

看到View的源码中,比如说mLeft,注释中说mLeft是从父布局的左边缘到这个View的左边的像素。

/**
 * The distance in pixels from the left edge of this view's parent
 * to the left edge of this view.
 * {@hide}
 */
@ViewDebug.ExportedProperty(category = "layout")
protected int mLeft;

这四个坐标是相对于这个View的父容器来说的,所以它是一种相对坐标。


View中提供了四个get()来获得这四个参数,比如下面的getTop()。

/**
 * Top position of this view relative to its parent.
 *
 * @return The top of this view, in pixels.
 */
@ViewDebug.CapturedViewProperty
public final int getTop() {
    return mTop;
}

可以从上面的四个参数计算出View的宽高。

width = right - left;
height = bottom - top;

getTanslationX() getTranslationY()

Android3.0之后提供的两个方法,getTranslationX()和getTranslationY(),它们不同于上面的四个参数,这两个参数会由于 View的平移而变化,表示View左上角坐标相对于left、top(原始左上角坐标)的偏移量。

/**
 * The horizontal location of this view relative to its {@link #getLeft() left} position.
 * This position is post-layout, in addition to wherever the object's
 * layout placed it.
 *
 * @return The horizontal position of this view relative to its left position, in pixels.
 */
@ViewDebug.ExportedProperty(category = "drawing")
public float getTranslationX() {
    return mRenderNode.getTranslationX();
}

getX() getY()

Android3.0之后提供了getX()和getY()两个方法。

/**
 * The visual x position of this view, in pixels. This is equivalent to the
 * {@link #setTranslationX(float) translationX} property plus the current
 * {@link #getLeft() left} property.
 *
 * @return The visual x position of this view, in pixels.
 */
@ViewDebug.ExportedProperty(category = "drawing")
public float getX() {
    return mLeft + getTranslationX();
}

代码是将mLeft加上translationX得到x的,可以看出来,x和y代表的就是当前View左上角相对于父布局的偏移量。

上面三组参数可以得到两组等式。

x = left + translationX;
y = top + translationY

MotionEvent

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

  • ACTION_DOWN——手指刚接触屏幕。
  • ACTION_MOVE——在屏幕上移动。
  • ACTION_DOWN——从屏幕上松开。

这些事件对应MotionEvent类中的几个静态常量。

public static final int ACTION_DOWN = 0;
public static final int ACTION_UP   = 1;
public static final int ACTION_MOVE = 2;

正常情况下的一些列点击事件:

  • 点击屏幕后立即松开,ACTION_DOWN->ACTION_UP
  • 点击屏幕滑动后再松开,ACTION_DOWN->ACTION_MOVE->......->ACTION_MOVE->ACTION_UP

可以通过MotionEvent对象调用getX()、getY()、getRawX()、getRawY()获取触碰点的位置参数。

  • getX()、getY() 相对于当前View左上角的x、y值。
  • getRawX()、getRawY() 相对于手机屏幕左上角的x、y值。

这四个方法都是去调用native方法。

public final float getRawX() {
    return nativeGetRawAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
}

@FastNative
private static native float nativeGetRawAxisValue(long nativePtr,
        int axis, int pointerIndex, int historyPos);

TouchSlop

TouchSlop是系统能识别的最小滑动距离,如果小于这个值,则不认为是滑动。这是一个常量和设备有关,可以通过以下方式获得。

ViewConfiguration.get(getContext()).getScaledTouchSlop();
public int getScaledTouchSlop() {
    return mTouchSlop;
}

这个mTouchSlop在ViewConfiguration的无参构造器中用一个常量赋了初始值为8。

private static final int TOUCH_SLOP = 8;
@Deprecated
public ViewConfiguration() {
    //...
    mTouchSlop = TOUCH_SLOP;
    //...
}

有参构造器中初始化为资源文件的一个值,这个值也是8。

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

private ViewConfiguration(Context context) {
    //...
    mTouchSlop = res.getDimensionPixelSize(com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
    //...
}

在处理滑动的时候可以使用这个值来做一些过滤,过滤掉滑动距离小于这个值,会有更好的用户体验。

Velocity Tracker

用来获取手指滑动过程中的速度,包括水平速度和垂直速度。

用法

在onTouchEvent()中追踪当前单击事件的速度。

  1. 首先获得一个VelocityTracker对象,再将当前时间加入进去。
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
  1. 计算自定义时间内的速度,再调用get获得定义时间内划过的像素点。

int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
  1. 计算真正的速度。
int xV = xVelocity / 1;//这里的1是上面计算时间时定义的时间间隔1000ms
int yV = yVelocity / 1;
  1. 回收资源。
velocityTracker.clear();
velocityTracker.recycle();

注意

  • 获取速度之前必须要调用computeCurrentVelocity()计算速度。
  • getXVelocity()\getYVelocity()获取到的是计算单位时间内滑过的像素值,并不是速度。

GestureDetector

GestureDetector用于检测用户的单击、滑动、长按、双击等行为。

GestureDetector内部有两个监听接口,OnGestureListener和OnDoubleTapListener,里面的方法可以根据需求去实现。

public interface OnGestureListener {
    boolean onDown(MotionEvent e);//手指轻轻触摸屏幕的一瞬间,一个ACTION_DOWN触发
    void onShowPress(MotionEvent e);//手指轻触屏幕,没有松开或挪动
    boolean onSingleTapUp(MotionEvent e);//轻触后松开,单击行为,伴随一个ACTION_UP触发
    boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);//拖动行为,由一个ACTION_DOWN和一系列ACTION_MOVE触发
    void onLongPress(MotionEvent e);//长按
    boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);//按下快速滑动后松开,一个ACTION_DOWN、多个ACTION_MOVE和一个ACTION_UP触发
}
public interface OnDoubleTapListener {
    boolean onSingleTapConfirmed(MotionEvent e);//严格的单击行为,不能是双击中的其中一次单击,onSingleTapUp可以是双击中的其中一次。
    boolean onDoubleTap(MotionEvent e);//双击,两次单击,不可能和onSingleTapConfirmed共存
    boolean onDoubleTapEvent(MotionEvent e);//双击行为,双击期间ACTION_DOWN ACTION_MOVE ACTION_UP都会触发此回调。
}

使用

创建一个GestureDetector,根据需要实现接口并传入GestureDetector。

gestureDetector = new GestureDetector(context, gestureListener);
gestureDetector.setOnDoubleTapListener(doubleTapListener);
gestureDetector.setIsLongpressEnabled(false);//解决长按屏幕后无法拖动的现象

接管View的onTouchEvent(),GestureDetector的onTouchEvent()中会根据event来回调上面说的两个接口方法。

@Override
public boolean onTouchEvent(MotionEvent event) {
    boolean consume = gestureDetector.onTouchEvent(event);
    return consume;
}

注意

并不是必须要用GestureDetector来实现所需的监听,完全也可以直接在View的onTouchEvent()中做判断并实现需求。所以,如果只需要监听简单的单击事件就可以直接使用View的onTouchEvent(),如果需要监听复杂一点的一系列事件,就可以使用GestureDetector。

View的滑动

scrollTo()/scrollBy()

scrollTo和scrollBy可以改变View内容的位置,举例来说就是如果对ViewGroup调用scrollTo只会改变其子View的位置,如果对View,比如TextView调用,那么只会改变这个TextView文字的位置。

1. 使用

tv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        bt.scrollTo(100, 200);
        tv.scrollBy(-5, -5);
    }
});

直接使用View对象去调用两个方法,传入位移像素值就可以了。scrollTo()是内容的绝对移动,scrollBy()是内容的相对移动。

但是需要注意的是,这两个方法在onCreate()中调用,可能不会成功,原因应该是因为那时View还没有完全加载完毕,所以调用会不起作用。

2. scrollTo的实现

/**
 * Set the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the x position to scroll to
 * @param y the y position to scroll to
 */
public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

这里有两个量,mScrollX和mScrollY:

/**
 * The offset, in pixels, by which the content of this view is scrolled
 * horizontally.
 * {@hide}
 */
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;

mScrollX表示View内容和View本身的横向偏移量,mScrollY就是纵向偏移的像素值了。

  1. scorllTo()首先比较内容偏移量和传入的x y是否相等,都不相等再操作。
  2. 它记录了原始的两个偏移量,之后将传入的x y赋值给mScrollX和mScrollY。
  3. 接着调用了invalidateParentCaches(),方法注释意思是当启动了硬件加速时去通知此View的父容器清除缓存。
  4. 调用了onScrollChanged(mScrollX, mScrollY, oldX, oldY),这个方法内部会判断我们是否有设置OnScrollChangeListener,如果有就调用它的回调方法。
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    //......
    if (mListenerInfo != null && mListenerInfo.mOnScrollChangeListener != null) {
        mListenerInfo.mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt);
    }
}
  1. awakenScrollBars()唤醒scrollbar去重新绘制,如果失败返回false,就直接调用postInvalidateOnAnimation()重新绘制。所以不管怎么样最终都会调用到postInvalidateOnAnimation()。
public void postInvalidateOnAnimation() {
    // We try only with the AttachInfo because there's no point in invalidating
    // if we are not attached to our window
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);
    }
}

判断与Window的连接是否空,不空就调用ViewRootImpl的dispatchInvalidateOnAnimation()。

public void dispatchInvalidateOnAnimation(View view) {
    mInvalidateOnAnimationRunnable.addView(view);
}

将这个View加入到了InvalidateOnAnimationRunnable这个Runnable中的集合中,在这个Runnable的run()中,遍历了集合中的每个View,调用View的invalidate()后释放。invalidate()就是去在UI线程中重绘View的,最后View就在新的位置显示了。

@Override
public void run() {
    //......
    for (int i = 0; i < viewCount; i++) {
        mTempViews[i].invalidate();
        mTempViews[i] = null;
    }
    //......
}

总结一下,简单来说逻辑就是改变mScrollX和mScrollY的值,之后刷新UI,显示在新位置。

3. scrollBy()的实现

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

scrollBy()就是调用了scrollTo,只不过参数加上了当前已有的偏移量。所以可以猜到scrollBy()是相对于当前偏移的基础上相对移动x y的像素值,而scrollTo()是相对于View的原始位置绝对移动。

4. mScrollX 和 mScrollY的正负

如下图所示,白色框是View自身的位置,灰色是View的内容移动后的位置,那么假设偏移量都为100,mScrollX的值就是100,mScrollY的值是100,单位是像素,都是正值。

下面View的内容移动到了右下角,此时mScrollX和mScrollY的值就是负的了。

动画

使用动画来移动View,可以使用View动画,也可以使用属性动画(3.0版本以下需要使用nineoldandroid)。

1. 使用View动画

首先可以在xml中定义一个动画集合。

<?xml version="1.0" encoding="utf-8"?>
<set 
    android:fillAfter="true"
    android:zAdjustment="normal">

    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="100"
        android:toYDelta="100"
        android:interpolator="@android:anim/linear_interpolator"/>

</set>

这个动画会让View从原始位置向右下方平移100个像素。

再对View对象开始动画,传入加载进来的上面写的动画。

tv.startAnimation(AnimationUtils.loadAnimation(MainActivity.this, R.anim.anim_view_event));

2. 使用属性动画

使用ObjectAnimator类去设置动画。

ObjectAnimator.ofFloat(tv, "translationX", 0, 10).setDuration(100).start();

3. 注意

  • 使用View动画其实并不是改变View的真正位置,而是移动View的影像,不会改变View的真实位置参数。

这就会导致一个问题,如果View有点击事件,新位置并不能触发点击事件,而是原位置仍能触发,尽管View看起来已经不在原先的位置上了。

  • 属性动画改变View本身属性只能兼容到Android3.0,所以如果需要兼容更低的版本,就必须要使用开源动画库nineoldandroid。

改变布局参数

使用

MarginLayoutParams params = (MarginLayoutParams) tv.getLayoutParams();
params.leftMargin += 100;
tv.requestLayout();
//tv.setLayoutParams(params); 也可以使用这个重新设置参数

改变布局参数的方法可以通过更改margin来改变View的位置达到移动的效果,这种方法需要根据实际去做不同的处理。

滑动对比

滑动方式 优点 缺点
scrollTo() / scrollBy() 简单易使用,不影响点击事件 只能移动View的内容,不能移动View本身
View动画 能够实现复杂的效果 只能改变View的影像,会影响View的点击事件
属性动画 3.0以上移动View本身,能够实现复杂的效果 3.0以下不能改变View本身属性,需要nineoldandroid来兼容
改变参数 不会影响点击事件,改变的是View自身的属性 使用稍麻烦,需要根据需求来灵活应用

再总结一下适用场景:

  • scrollTo() / scrollBy(): 操作简单,适合对于View的内容的移动。
  • 动画:操作简单,主要适用于对没有交互的移动和复杂的动画效果。
  • 改变参数:操作稍微复杂,适用于有交互的移动。

弹性滑动

前面的方法其实只能叫做移动,并不能叫滑动。弹性滑动有一个共同的思想,在一段时间内将一次大的滑动分成若干次小的滑动来完成。

Scoller

Scroller本身无法实现弹性滑动,需要和View的computeScroll()配合使用。在最后通过分析可以发现也是通过scrollTo()实现滑动的,所以它也是View内容的滑动,而不是View本身的滑动。

使用

自定义一个TextView,实现TextView的文字向手指点击的地方弹性滑动。

public class MyTextView extends TextView {

    private Scroller mScroller;
    private int xDown;
    private int yDown;

    //...

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);//初始化Scroller对象
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN://记录点击的相对坐标
                xDown = (int) event.getX();
                yDown = (int) event.getY();
                break;
            case MotionEvent.ACTION_UP:
                smoothScroll(-xDown, -yDown);//调用自定义的弹性滑动

        }
        return true;
    }

    //自定义的弹性滑动方法
    public void smoothScroll(int destX, int destY) {
        //画的初始滑动偏移
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        //计算需要滑动的两个方向的大小
        int deltaX = -destX - scrollX;
        int deltaY = -destY - scrollY;
        调用Scroller对象的startScroll()
        mScroller.startScroll(scrollX, scrollY,  deltaX, deltaY, 1000);
        invalidate();//重绘
    }

    //固定的重写compuuteScroll
    @Override
    public void computeScroll() {
        if  {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
}
  1. 初始化Scroller对象。
  2. 实现computeScroll()。
  3. 自定义弹性滑动的方法,内部调用Scroller对象的startScroll()、invalidate()。
  4. 就可以调用自定义的弹性滑动方法进行弹性滑动了。

实现

1. 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()只是进行了一些计算和参数的记录,并没有进行真正的滑动工作。四个参数分别是其实位置的x、y坐标,x、y方向的滑动距离,滑动的时间间隔。

2. invalidate()

invalidate()会导致View的重绘调用View的draw(),View的draw()中又会去调用computeScroll(),computeScroll()在View中是一个空实现,所以需要我们自己去实现。

3. computeScroll()

public void computeScroll() {
    if  {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}
  • 如果想实现弹性滑动这样的需求,其实computeScroll()的实现和上面写成一样就可以了,不需要做其他的改动。发现在这个方法里,还是调用了scrollTo(),所以Scroller弹性滑动也是用scrollTo()实现的。

  • 就能猜到computeScrollOffset()是用来计算CurrX和CurY的,也就是最初提到的将一个大滑动拆分成小滑动,computeScrollOffset()就是去计算每一次小滑动的坐标的。

  • 最后调用postInvalidate()进行下一次重绘,重复之前的操作。

4. computeScrollOffset()

最后再来单独看一下computeScrollOffset()的实现。

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;
        //...
        }
    } else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}
  • 它首先判断是否完成,如果已经完成就直接返回false。
  • 如果还没完成,计算过去的时间,如果还有剩余,就根据时间百分比计算下一个滑动位置,返回true。
  • 如果已经超过时间,就赋值下一个滑动位置为目标位置,并将mFinished变成true,返回true。
  • 在调用computeScrollOffset()的地方,如果computeScrollOffset()返回了true就进行scrollTo()并重新绘制。

动画属性

除了利用Scroller的computeScrollOffset()来分成小份计算位移,还可以利用动画属性。前面介绍的View动画和属性动画都属于弹性动画。除了直接使用动画,还可以利用动画的特性。

使用

final int startX = 0;
final int deltaX = -100;
final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float fraction = animation.getAnimatedFraction();
        tv.scrollTo(startX + (int)(deltaX * fraction), 0);
    }
});

tv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        animator.start();
    }
});

利用动画的回调,实现像Scroller类似的,在动画改变的时候通过onAnimationUpdate()监听,获得百分比,调用scrollTo()滑动一小步,也是View内容的滑动。

延时策略

通过发送延时消息从而达到渐近式的效果。可以使用Handler、View的postDelayed()、Thread的sleep()。具体的思路其实和上面是一样的,只不过这里需要自己去实现延时,而上面的方法已经内部实现,只需要计算小段位移后进行小段滑动就可以了。

显示全文