我们平时会经常遇到View的滑动,不管自定义View,还是动画,都需要这个东西,但是往往最熟悉的最陌生。
View滑动可以分为三大类
1、自身的ScrollTo和ScrollBy
2、通过动画给View平移效果
3、改变View的LayoutParams进行修改View的位置
很传统的三大分类。但是我们要思考为什么要分出这三类,各自的实现意义。
一、ScrollTo和ScrollBy。
首先看源码:
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
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();
}
}
}
postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
其实都是本质都是ScrollTo方法。我们看一下mScrollX和mScrollY的含义是什么?
View里的内容在横向的偏移像素量。记住View和View里的内容。
就好比:view = 盒子 内容 = 盒子里的东西,我们改变的是盒子里的东西的位置,而不是这个盒子的位置。所以我们在运用这个方法的时候,一定是移动View里的子View。而不是这个View本身。
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
我们来看一下具体实现,x,y就是我们要scroll的位置,首先要把之前scroll的值赋为oldX,oldY,也就是之前的偏移量,那么如果我们要到达x,y,就需要计算差值,也就是:
desX - oldX = 移动的值 =dx。
如果我们之前的偏移量为100,100,ScrollTo(200,200),那么移动的值就是 200-100 = 100;需要在移动100,而不必从原始位置再到目的位置了,而是从现有位置移动到目的位置。
那么这个移动的方向是基于什么的呢?通过实际测试,我们发现是
View左边界 - View内容左边界 = scrollX
View上边界 - View内容上边界 = scrollY
这么一说 我们发现当scrollX 为正的时候,我们看到内容的移动方向是反方向,其实就去想象你自己用手去拉这个黄纸,左边和上边为正方向,得到的值就是scroll的值。
当我们在运用的时候一定要切记
1、如果我们想让这个View移动,那么一定要在它的父View里运用这个方法。
2、一定要是反方向
点击子view,在父View(全屏View)中移动。
点击view,内容在其内移动。
至于代码大家自己可以亲自实现一下。
但是我们平时移动不会这么僵硬的,更多时候需要Scroller来实现一些阻尼效果。既然讲到这里了,不讲Scroller肯定是过不去的了。
Scroller
用法很简单:
- 创建Scroller的实例
- 调用startScroll()方法来初始化滚动数据并刷新界面
- 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
没啥需要多说的,我们借用郭神的例子来实际探究一下Scroller的魅力所在。
我们要模仿的是ViewPager的效果。
ezgif.com-video-to-gif.gif首先我们要具备基本的自定义View的知识不了解的可以看一下 谷哥的小弟大神的系列文章。
public class XViewPage extends ViewGroup {
/**
* 手指此刻所在的屏幕坐标
*/
private float mMoveX;
/**
* Scroller 的实例
*/
private Scroller mScroller;
/**
* 判定拖动的最小距离像素值
*/
private int mTouchSlop;
/**
* 按下X坐标值
*/
private float mDownX;
/**
* 上一次触发ACTION_MOVE的X坐标值
*/
private float mLastMoveX;
/**
* 界面可滚动右边界
*/
private int rightBorder;
/**
* 界面可滚动左边界
*/
private int leftBorder;
public XViewPage(Context context) {
this(context, null);
}
public XViewPage(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public XViewPage(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledPagingTouchSlop();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//把这几个子控件平铺
childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
//初始化左右边界
leftBorder = getChildAt(0).getLeft();
rightBorder = getChildAt(getChildCount() - 1).getRight();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int chileCount = getChildCount();
for (int i = 0; i < chileCount; i++) {
View childView = getChildAt(i);
//测量每一个子View的大小
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getRawX();
mLastMoveX = mDownX;
break;
case MotionEvent.ACTION_MOVE:
mMoveX = ev.getRawX();
float diff = Math.abs(mMoveX - mDownX);
if (diff > mTouchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mMoveX = event.getRawX();
int scrolledX = (int) (mLastMoveX - mMoveX);
if (getScrollX() + scrolledX < leftBorder) {
scrollTo(leftBorder, 0);
return true;
} else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
scrollTo(rightBorder-getWidth(), 0);
return true;
}
scrollBy(scrolledX, 0);
mLastMoveX = mMoveX;
break;
case MotionEvent.ACTION_UP:
//当手抬起来的时候 根据当前的滚动值 来判断应该滚动到哪一个子view的界面
int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
int dx = targetIndex * getWidth() - getScrollX();
//调用startScroll() 来初始化滚动数据 并刷新界面
mScroller.startScroll(getScrollX(), 0, dx, 0);
postInvalidate();
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
}
总结一下过程:
1、重写onMeasure() 测量每一个子View的大小
2、重写onLayout() 横向平铺这些子View
3、 重写 onTouchEvent 和 onInterceptTouchEvent的 事件分发处理
4、重写 computeScroll()实现mScroller的scrollTo方法,达到顺滑的效果
我们发现ScrollTo在Android中的而运用实在是太广泛了。
动画给View平移效果
这个我们在做交互,转场动画的时候经常会用到。特别是3.0以上的属性动画,解决了身形不同的问题。
当3.0以下的时候,采用nineoldndroids这个开源动画,效果和属性动画一致,不同的是移动的只是一个影像而不是本体,在点击事件方面有着天生的缺陷,解决的办法也只能在以后后的位置隐藏一个相同的View来显示并做事件处理,这样移花接木的手段。
ObjectAnimator animator = ObjectAnimator.ofFloat(imageView, "alpha", 1f, 0f);
animator.setDuration(1000);//时间1s
animator.start();
动画我们将在之后的文章中详细讲解,这里就不多介绍了。
改变布局参数
这是一个治本的方式。可以对View的大小位置坐各种改变。
ViewGroup.LayoutParams params = view.getLayoutParams();
ViewGroup.MarginLayoutParams marginParams = null;
if (params instanceof ViewGroup.MarginLayoutParams) {
marginParams = (ViewGroup.MarginLayoutParams) params;
} else {
marginParams = new ViewGroup.MarginLayoutParams(params);
}
marginParams.height = 500;
marginParams.setMargins(10, 200, 30, 40);
view.setX(100);
view.setLayoutParams(marginParams);
// view.requestLayout();
最后
总结一下三个滑动的使用场景
1、scrollX 和 scrollY 操作简单,适合对View的内容进行滑动(比如模仿ViewPager)
2、动画,操作简单适合,没有交互的View,实现复杂的动画效果
3、改变布局和View的参数,比较复杂,但是适用于有交互的View
大家根据不同需求选择和配合使用。
参考:
- https://blog.csdn.net/guolin_blog/article/details/48719871
- 任玉刚《Android开发艺术探索》第三章
网友评论