View 的基础知识与滑动

参考资料:
1.《android开发艺术探索》

View 是Android中所有控件的基类,表示具体的一个控件;ViewGroup为容器类,可包含多个View类;View 跟Viewgroup的设计采用的组合模式;

View的位置参数#

View的位置主要由她的4个顶点来决定,对应view的4个属性:left、top、right、bottom;

View的位置信息

从图得出:
width = right-left;
height = bottom - top;

从Android3.0开始,View增加额外的几个参数:x,y,translationX和translationY, 其中x,y是view的左上角坐标,translationX和translationY是View 左上角相对于父容器的偏移;
换算关系如下:
x = left + translationX;
y = top + translationY;
注意: View在平移过程中,top和left表示的是原始左上角的位置信息,值不会发生变化,发生变化的是x、y,translationX与translationY;

View的滑动#

可通过3种方式来实现View的滑动:

  1. 通过View本身提供的 scrollTo/scrollBy来实现滑动;
  2. 通过平移动画来实现滑动;
  3. 改变View的LayoutParams使得View重新布局来实现动画;

scrollTo/scrollBy##

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

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

scrollTo/by改变的mScrollX/Y的值
scrollTo 是绝对位置移动,scrollBy是基于上一次的位置移动;
在滑动过程中:mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,mScrollY的值等于View上边缘和View内容上边缘在竖直方向上的距离;
View的边缘指的是View的位置,由4个顶点组成,scrollTo/scrollBy只能改变View内容的位置,而不能改变View在布局中的位置;
记住:是相对于内容边缘而言的,因为scroll只改变View内容的位置
当View的左边缘在View内容边缘的右边时,mScrollX为正,mScrollY与之类似;
即:改变的是内容区域,实际区域并没有发生变化;

例子

 findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(TAG, String.format("scrollX:%s, left:%s, x:%s", iv.getScrollX(), iv.getLeft(), iv.getX()));
                // 100 表示实际区域距离内容区域100个像素(右), 即:内容区域,左移动100个像素
                iv.scrollTo(100, (int) iv.getY());     
                Log.e(TAG, String.format("scrollX:%s, left:%s, x:%s", iv.getScrollX(), iv.getLeft(), iv.getX()));
            }
        });

输出:可以看到left值没有发生变化,即:实际区域不变;

scrollTo

注意:

scrollTo/scrollBy 与 setX/setY(setTranslation) 完全不是一码事情,scroll 是在本view进行内容区域位置的改变,而set是设置view在父View中的位置,他会修改left 等4个布局参数的值;

使用动画##

View 动画是对View的影像做操作,并不能改变View的位置信息,包括宽、高,真身还是在原来的位置;3.0 以上, 可使用属性动画来解决这个问题;

 Animation anim = new TranslateAnimation(iv.getX(), 100, iv.getY(), iv.getY());
anim.setFillAfter(true);
 iv.startAnimation(anim);

改变布局参数##

第3种实现View滑动的方式,改变布局参数,即改变LayoutParams.

ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) iv.getLayoutParams();
lp.leftMargin += 100;
iv.requestLayout();  // 或者 iv.setLayoutParams(lp);

setX setTranslationX 的区别##

通过源码,可以发现, 实际上就是调用了setTranslationX,默认情况下,getX() 等于 mLeft,即View左边缘距离父容器左边缘的距离;

 public void setX(float x) {
      setTranslationX(x - mLeft);
 }

 public float getX() {
        return mLeft + getTranslationX();
 }

translationX 表示相对于原始位置的X轴上的移动距离,比如:原始mLeft=100,设置translation为-50后,getX(): 返回50; getTranslation返回-50;

Log.e(TAG, String.format("scrollX:%s, left:%s, x:%s, y:%s, translateX:%s", iv.getScrollX(), iv.getLeft(), iv.getX(), iv.getY(), iv.getTranslationX()));
//setX与setTranslationX
iv.animate().translationX(-50).start();
Log.e(TAG, String.format("scrollX:%s, left:%s, x:%s, y:%s, translateX:%s", iv.getScrollX(), iv.getLeft(), iv.getX(), iv.getY(), iv.getTranslationX()));

输出(可以看到原始信息mLeft没有发生改变):

tranlateX

记录开发中一些标记值

滑动时(偏移计算)

如下代码:

onTouchEvent() ....  
switch (action) {
    case MotionEvent.ACTION_DOWN:
            mLastY = y;    
    case MotionEvent.ACTION_MOVE:
           float dy = event.getY() - mLastY;    // 移动距离
           Log.e("TAG", "dy = " + dy + " , y = " + y + " , mLastY = " + mLastY);

往上移动时(上移)y递减;反之(下移)y递增;
dy(上移)为负,反之(下移)为正;
dx(左移)为负,右移为正

移动方法(scrollTo/scrollBy)

scrollBy(0, dy); dy为正时,往上移动,反之(负时)往下移动,一般在滑动时,根据手指移动时,我们取反,即,这样调用:scrollBy(0, -dy)
scrollTo 类似;
scrollBy(dx, 0) 时,dx 为正,往左移动,反之(负)向右移动;

是不是感觉特麻烦了,那设置 dy/ dx 的时候,直接用 mLast - 当前的;

然后 scrollBy 直接设置值即可####

float dy =  mLastY - event.getY();
scrollBy(0, dy);

已滑动距离 (getScrollY() /getScrollX())

getScrollY()为正时,即view往上偏移了;为 0 时,位置未发生了变化;为负即view往下偏移了;
getScrollX()为正时,即View往左偏移了;其他类似;

加速度(velocityX/velocityY)的正反

如下代码 :

 velocityTracker?.let {
         it.computeCurrentVelocity(1000, maxVelocity?.toFloat())
         Log.e("better", "x = ${it.xVelocity}, y = ${it.yVelocity}")
         if (Math.abs(it.xVelocity) > minVelocity)  // 加速度
               scroller.fling(scrollX, 0, -velocityX.toInt(), 0, 0, topViewHeight, 0, 0)
               invalidate()
        }

xVelocity 为正,右滑动,负时,左滑动;
yVelocity为正,下滑动,负时,向上滑动;
注意: 但是 scroller启用时,最终走的还是 computeScroll,即:scrollTo,根据上面的,所以需要传值,需要取反;

View是否滑动到边界(这里不包含padding切记)

// 
ViewCompat.canScrollVertically(scrollView, -1)    // 是否还能下拉
!ViewCompat.canScrollVertically(scrollView, -1)    // 不能下拉了(再继续下拉)
ViewCompat.canScrollVertically(scrollView, 0)  // 是否还能上拉

ViewCompat.canScrollHorizontally(scrollView, -1)  // 是否还能右拉
!ViewCompat.canScrollHorizontally(scrollView, -1)  // 不能右拉(但继续右拉)

scroller 与 Velocity 相关的代码片段

// scroller的释放,ACTION_DOWN
 override fun onTouchEvent(event: MotionEvent?): Boolean {
    when (it.action) {
                MotionEvent.ACTION_DOWN -> {
                    lastY = y
                    scroller?.let {
                        if (!it.isFinished) {
                            it.abortAnimation()
                            return true
                        }
                    }
                }


// 调用
MotionEvent.ACTION_UP -> {
      velocityTracker?.let {
               it.computeCurrentVelocity(1000, maxFlingVelocity?.toFloat() ?: 0f)
                   if (Math.abs(it.yVelocity) > minFlingVelocity?.toFloat() ?: 0f) {
                         scroller.fling(0, scrollY, 0, it.velocityY, 0, 0, 0, topHeight ?: 0)
                  }
        }
       releaseVelocity()
  }

// 没有结束,继续移动 
override fun computeScroll() {
        scroller?.let {
            if (it.computeScrollOffset()) {
                scrollTo(0, it.currY)
                invalidate()
            }
        }
    }

View 实现 GestureDetector 手势的监听方法回调

distanceX 右划为负数()
distanceY 下划为负

 override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
 //当正在进行缩放时,不触发滚动
    if (scaleGestureDetector.isInProgress) {
              return false
    } else {
           scrollRange(distanceX, distanceY)   //  ,
            return true
        }
 }

// ======= onScroll 回调越界的处理 ========
val horizontalScrollRange = computeHorizontalScrollRange()  // 横向滚动范围
val verticalScrollRange = computeVerticalScrollRange()  // 纵向滚动范围
//横轨滚动越界
var distanceX = distanceX.toInt()
if (0 > scrollX || scrollX > horizontalScrollRange) {   // 避免直接越界
       //横向直接越界
       distanceX = 0
} else if (scrollX < -distanceX) {   
        //横向向左滚动阀值越界
        distanceX = -scrollX
} else if (scrollX + distanceX > horizontalScrollRange) {
        //横向向右越界
        distanceX = horizontalScrollRange - scrollX
}

//纵向滚动越界
var distanceY = distanceY.toInt()
if (0 > scrollY || scrollY > verticalScrollRange) {
        distanceY = 0
} else if (scrollY < -distanceY) {
        distanceY = -scrollY
} else if (scrollY + distanceY > verticalScrollRange) {
        distanceY = verticalScrollRange - scrollY
}
scrollBy(distanceX, distanceY)

你可能感兴趣的:(View 的基础知识与滑动)