参考资料:
1.《android开发艺术探索》
View 是Android中所有控件的基类,表示具体的一个控件;ViewGroup为容器类,可包含多个View类;View 跟Viewgroup的设计采用的组合模式;
View的位置参数#
View的位置主要由她的4个顶点来决定,对应view的4个属性:left、top、right、bottom;
从图得出:
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的滑动:
- 通过View本身提供的 scrollTo/scrollBy来实现滑动;
- 通过平移动画来实现滑动;
- 改变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/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没有发生改变):
记录开发中一些标记值
滑动时(偏移计算)
如下代码:
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)