Android View(一)——View的基础知识

一.View的基础知识

1.什么是View

View是一种界面层的控件的一种抽象,它代表了一个控件,是Android中所有控件的基类。
在这里插入图片描述

2.View的位置参数

在这里插入图片描述
在这里插入图片描述

3.MotionEvent

在我们触摸屏幕的过程中,可以分为三种情况,分别是按下、滑动、弹起。Android中为我们封装好了一个MotionEvent类,使得我们对屏幕的一系列操作事件都可以记录在这个MotionEvent里面。

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

通过MotionEvent对象我们可以得到点击事件发生的x和y坐标

  • getX/getY 相对当前View左上角的x和y坐标
  • getRawX/getRawY 相对手机屏幕左上角的x和y坐标

事件序列:由一个ACTION_DOWN事件,0个或者1个或者多个ACTION_MOVE事件,加上一个ACTION_UP事件组成的一个序列

4. TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,这是一个常量,和设备有关,在不同设备上这个值可能不同
ViewConfiguration这个类主要定义了UI中所使用到的标准常量,像超时、尺寸、距离,如果我们需要得到这些常量的数据,我们就可以通过这个类来获取,具体方法如下:
获取ViewConfiguration实例:

 ViewConfiguration viewConfiguration = ViewConfiguration.get(Context);

常用的方法

//  获取touchSlop (系统 滑动距离的最小值,大于该值可以认为滑动)
int touchSlop = viewConfiguration.getScaledTouchSlop();
//  获得允许执行fling (抛)的最小速度值
int minimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
//  获得允许执行fling (抛)的最大速度值
int maximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
//  Report if the device has a permanent menu key available to the user 
//  (报告设备是否有用户可找到的永久的菜单按键)
//  即判断设备是否有返回、主页、菜单键等实体按键(非虚拟按键)
boolean hasPermanentMenuKey = viewConfiguration.hasPermanentMenuKey();  

5. VelocityTracker

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

//VelocityTracker是Android系统内置的速度追踪类,首先调用它来追踪当前
//点击事件的速度,event一般是通过onTouchEvent函数传递的MotionEvent对象
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

//先调用computeCurrentVelocity函数,用于设定计算速度的时间间隔
velocityTracker.computeCurrentVelocity(1000);
//这里的速度指的是一段时间内手指划过的像素数,比如将时间间隔设置为1000ms,
//在1s内,手指在水平方向划过100像素,水平速度就是100.
//  速度的计算为(终端位置-起始位置)/间隔时间。
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

//不需要时,调用clear方法回收并重置内存
velocityTracker.clear();
velocityTracker.recycle();

6. GestureDetector

手势检测,用于辅助检测用户的单击,滑动,长按,双击等行为,使用系统的GestureDector来监听这些事件

GestureDetector内部的Listener接口:

监听器 简介
OnGestureListener 手势检测,主要有:按下(Down)、快速滑动(Fling)、长按(LongPress)、滚动(Scroll)、触摸反馈(ShowPress)和单击抬起(SingleTapUp)
OnDoubleTapListener 双击检测,主要有三个回调类型:双击(DoubleTap)、单击确认(SingleTapConfirmed)和双击事件回调(DoubleTapEvent)
OnContextClickListener 这是Android6.0(23)才添加的,用于检测外部设备上按钮是否按下,一般情况下可以忽略
SimpleOnGestureListener 该类是GestureDetector提供给我们的一个更方便非响应不同手势的类,这个类实现了上述三个接口(但是所有方法都是空的),该类是static类,也就是说它是一个外部类。可以在外部继承这个类,重写里面的手势处理方法。

使用:

  • 实现OnGestureListener/OnGestureListener/SimpleOnGestureListener接口
  • 实例化GestureDetectorCompat类
  • 接管目标View的OnTouchEvent方法
GestureDetector.OnGestureListener listener = new GestureDetector.OnGestureListener() {
    
    
            @Override
            public boolean onDown(MotionEvent e) {
    
    
                //手指按下的瞬间
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {
    
    
                //手指触摸屏幕,并且尚未松开或拖动。与onDown的区别是,onShowPress强调没用松开和没有拖动
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
    
    
                //手指离开屏幕(单击)
                return false;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    
    
                //手指按下并拖动,当前正在拖动
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {
    
    
                //手指长按事件
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    
    
                //手指快速滑动
                return false;
            }
        };
        mGestureDetector = new GestureDetector(this,listener);

        //防止长按后无法拖动的问题
        mGestureDetector.setIsLongpressEnabled(false);
        
	@Override
    public boolean onTouchEvent(MotionEvent event) {
    
    
        //既然要让GestureDetector来识别各种动作事件,那么就得让GestureDetector来接管事件管理,即在onTouchEvent里面只写入如下代码
        return mGestureDetector.onTouchEvent(event);
    }

二.View的滑动

View的滑动的实现有3种方法:

  • 使用scrollTo/scrollBy
  • 使用动画
  • 改变布局参数

1. 使用scrollTo/scrollBy

scrollTo、scrollBy方法是View中的,因此任何的View都可以通过这两种方法进行移动。首先要明白的是, scrollTo、scrollBy滑动的是View中的内容(而且还是整体滑动),而不是View本身。我们的滑动控件如SrollView可以限定宽、高大小,以及在布局中的位置,但是滑动控件中的内容(或者里面的childView)可以是无限长、宽的,我们调用View的scrollTo、scrollBy方法,相当于是移动滑动控件中的画布Canvas,然后进行重绘
 

1.1 getScrollX()、getScrollY()

getScrollX()、getScrollY()得到的是偏移量,是相对自己初始位置的滑动偏移距离,只有当有scroll事件发生时,这两个方法才能有值,否则getScrollX()、getScrollY()都是初始时的值0,而不管你这个滑动控件在哪里。所谓自己初始位置是指,控件在刚开始显示时、没有滑动前的位置。以getScrollX()为例,其源码如下:

public final int getScrollX() {
    return mScrollX;		
}

可以看到getScrollX()直接返回的就是mScrollX,代表水平方向上的偏移量,getScrollY()也类似。偏移量mScrollX的正、负代表着,滑动控件中的 内容相对于 初始位置在水平方向上偏移情况,mScrollX为正代表着当前内容相对于初始位置向左偏移了 mScrollX的距离,mScrollX为负表示当前内容相对于初始位置向右偏移了mScrollX的距离。 这里的坐标系和我们平常的认知正好相反。

1.2 scrollTo/scrollBy

scrollTo(int x,int y)移动的是View中的内容,而滑动控件中的内容都是整体移动的,scrollTo(int x,int y)中的参数表示View中的内容要相对于内容初始位置移动x和y的距离,即将内容移动到距离内容初始位置x和y的位置。正如前面所说,在处理偏移、滑动问题时坐标系和平常认知的坐标系是相反的。以一个例子说明scrollTo():
(1)调用scrollTo(100,0)表示将View中的内容移动到距离内容初始显示位置的x=100,y=0的地方,效果如下图:
在这里插入图片描述
(2)调用scrollTo(0,100)效果如下图:
在这里插入图片描述
源码展示:

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();
            }
        }
    }

scrollTo是相对于初始位置来进行移动的,而scrollBy(int x ,int y)则是相对于上一次移动的距离来进行本次移动。scrollBy其实还是依赖于scrollTo的,如下源码:

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

1.3 scrollTo实例使用

一个ViewGroup的实现类,实现手指水平滑动功能,这里只展示onTouchEvent里面的关键代码,完整代码后面给出

在这里插入图片描述

@Override
    public boolean onTouchEvent(MotionEvent event) {
    
    

        int x = (int) event.getX();		//相对当前View左上角的x坐标
        switch (event.getAction()){
    
    
            case MotionEvent.ACTION_DOWN:
                mLastX = x;			//记录开始滑动时当前View左上角的x坐标
                break;
            case MotionEvent.ACTION_MOVE:
                int dx = mLastX - x;            //本次手势滑动了多大距离
                int oldScrollX = getScrollX();  //原来的偏移量
                int preScrollX = oldScrollX + dx;//本次滑动后形成的偏移量
                if(preScrollX > (getChildCount() - 1) * getWidth()){
    
    
                    preScrollX = (getChildCount() - 1) * getWidth();
                }
                if(preScrollX < 0){
    
    
                    preScrollX = 0;
                }
                // scrollTo移动,注意只移动水平方向
                scrollTo(preScrollX,getScrollY());
                mLastX = x;
                break;
        }
        return true;
    }

2. 使用动画

通过使用动画,我们也可以实现一个View的平移,主要也是操作View的translationX和translationY,既可以补见动画,也可以采取属性动画。

关于动画:Android三种动画详解
 
普通动画

//layout下anim包中新建translate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
	android:fillAfter="true"
	android:zAdjustment="normal">
   <translate
   	android:duration="100"
   	android:fromXDelta="0"
   	android:fromYDelta="0"
   	android:interpolator="@android:anim/linear_interpolator"
   	android:toXDelta="100"
   	android:toYDelta="100" />
</set>

属性动画

ObjectAnimator.ofFloat(layout,"translationX",0,100).setDuration(1000).start();

注意:

  • View动画是对View的影像进行操作的。也就是说View动画并不能真正的改变View的位置。
  • 属性动画是真正改变View的位置,但它是从Android3.0开始推出的。

3. 改变布局参数

改变布局参数,即改变LayoutParams,比如:我们想把一个Button向右平移100px,我们只需将这个Button的LayoutParams里的marginLeft参数的值增加100px即可

MarginLayoutParams params = (MarginLayoutparamsmButton1.getLayoutParams();
	params.width + = 100;
	params.leftMargin + = 100;
	mButton1.requestLayout();
	//或者 mButton1.setLayoutParams(params);

还有一种做法:在将要移动的View前面,设置一个空的,默认宽度为0的View,若想要平均移动View,只需要设置空View的宽度即可

4. 各种滑动方式对比

在这里插入图片描述

三.弹性滑动

实现View的弹性滑动,即渐进式滑动。实现的方法很多,但都有一个共同的思想,将一次大的滑动分成若干次小的滑动,并在一个时间段中完成,下面就是常见的实现滑动的方法。

1.使用Scroller

两个重要的方法

方法名 解释
startScroll(int startX, int startY, int dx, int dy, int duration) 开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达偏移坐标为(startX+dx , startY+dy)处。
computeScrollOffset() 滑动过程中,根据当前已经消逝的时间计算当前偏移的坐标点,保存在mCurrX和mCurrY值中。

部分源码解读:

public class Scroller {
private int mStartX;//水平方向,滑动时的起点偏移坐标
private int mStartY;//垂直方向,滑动时的起点偏移坐标
private int mFinalX;//滑动完成后的偏移坐标,水平方向
private int mFinalY;//滑动完成后的偏移坐标,垂直方向

private int mCurrX;//滑动过程中,根据消耗的时间计算出的当前的滑动偏移距离,水平方向
private int mCurrY;//滑动过程中,根据消耗的时间计算出的当前的滑动偏移距离,垂直方向
private int mDuration; //本次滑动的动画时间
private float mDeltaX;//滑动过程中,在达到mFinalX前还需要滑动的距离,水平方向
private float mDeltaY;//滑动过程中,在达到mFinalX前还需要滑动的距离,垂直方向

public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}

/**
     * 开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达偏移坐标为(startX+dx , startY+dy)处
*/
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;
}

/**
     * 滑动过程中,根据当前已经消逝的时间计算当前偏移的坐标点,保存在mCurrX和mCurrY值中
     * @return
*/
public boolean computeScrollOffset() {
	if (mFinished) {//已经完成了本次动画控制,直接返回为false
		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);//计算出当前的滑动偏移位置,x轴
		mCurrY = mStartY + Math.round(x * mDeltaY);//计算出当前的滑动偏移位置,y轴
		break;
		...
            }
        }else {
		mCurrX = mFinalX;
		mCurrY = mFinalY;
		mFinished = true;
	}
	return true;
	}
    ...
}

Scroller类中最重要的两个方法就是startScroll()和computeScrollOffset(),但是Scroller类只是一个滑动计算辅助类,它的 startScroll()和computeScrollOffset()方法中也只是对一些轨迹参数进行设置和计算,真正需要进行滑动还是得通过View的scrollTo()、scrollBy()方法。为此,View中提供了computeScroll()方法来控制这个滑动流程。computeScroll()方法会在绘制子视图的时候进行调用。其源码如下:

/**  
 - Called by a parent to request that a child update its values for mScrollX  
 - and mScrollY if necessary. This will typically be done if the child is  
 - animating a scroll using a {@link android.widget.Scroller Scroller}  
 - object.  
 - 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制   
 */  
public void computeScroll() { //空方法 ,自定义滑动功能的ViewGroup必须实现方法体    
        
}   

因此Scroller类的基本使用流程可以总结如下:

  • 首先通过Scroller类的startScroll()开始一个滑动动画控制,里面进行了一些轨迹参数的设置和计算;
  • 在调用 startScroll()的后面调用invalidate();引起视图的重绘操作,从而触发ViewGroup中的computeScroll()被调用;
  • 在computeScroll()方法中,先调用Scroller类中的computeScrollOffset()方法,里面根据当前消耗时间进行轨迹坐标的计算,然后取得计算出的当前滑动的偏移坐标,调用View的scrollTo()方法进行滑动控制,最后也需要调用invalidate();进行重绘。

如下的一个简单代码示例:

@Override  
   public boolean onTouchEvent(MotionEvent ev) {  
       initVelocityTrackerIfNotExists();  
       mVelocityTracker.addMovement(ev);  
       int x = (int) ev.getX();  
       switch (ev.getAction()){  
           case MotionEvent.ACTION_DOWN:  
               if(!mScroller.isFinished()){  
                   mScroller.abortAnimation();  
               }  
               mLastX = x;  
               break;  
           case MotionEvent.ACTION_MOVE:  
               int dx = mLastX - x;  
               int oldScrollX = getScrollX();//原来的偏移量  
               int preScrollX = oldScrollX + dx;//本次滑动后形成的偏移量  
               if(preScrollX > (getChildCount() - 1) * getWidth()){  
                   preScrollX = (getChildCount() - 1) * getWidth();  
               }  
               if(preScrollX < 0){  
                   preScrollX = 0;  
               }  
               //开始滑动动画      
               mScroller.startScroll(mScroller.getFinalX(),mScroller.getFinalY(),dx,0);//第一步  
               //注意,一定要进行invalidate刷新界面,触发computeScroll()方法,因为单纯的startScroll()是属于Scroller的,只是一个辅助类,并不会触发界面的绘制  
               invalidate();  
               mLastX = x;  
               break;  
       }  
       return true;  
   }  
  
   @Override  
   public void computeScroll() {  
       super.computeScroll();  
       if(mScroller.computeScrollOffset()){//第二步  
           scrollTo(mScroller.getCurrX(),mScroller.getCurrY());//第三步  
           invalidate();  
       }  
   }  

2.使用动画

动画本身就是一种渐进的过程,因此通过它来实现太天然就具备弹性效果。比如,下面的代码就可以让一个VIew在100ms向右移动100像素

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

3.使用延时策略

通过发送一系列延时消息从而达到一种渐进式的效果,具体说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果,对于sleep方法来说,通过在while循环中不断地滑动View和sleep,就可以实现弹性滑动的效果。
以Handled为例,下面的代码将VIew的内容向左移动100像素。

private static final int MESSAGE_SCROLL_TO = 1;
    private static final int FRAME_COUNT = 30;
    private static final int DELAYED_TIME = 33;

    private int mCount;

    private Handler mHandler = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_SCROLL_TO: {
                    mCount++;
                    if (mCount <= FRAME_COUNT) {
                        float fraction = mCount / (float) FRAME_COUNT;
                        int scrollX = (int) (fraction * 100);
                        Button mButton;
                        mButton.scrollTo(scrollX,0);
                        mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
                    }
                    break;
                }
                default:
                    break;
            }

        }
    };

四.例子源码

ViewText:关于上面滑动的demo

猜你喜欢

转载自blog.csdn.net/haazzz/article/details/114334504