本文共 13504 字,大约阅读时间需要 45 分钟。
最近项目需求中要求给一个左右滑动的剧集列表添加阻尼回弹动画效果。大家都知道安卓原生的View一般滑动到View的边界之后就停止,不会有拖拽效果的,网上查找了一些方法,也跳了一些坑,今天就来总结下分享给大家。
当 View 滚动超过了 View 的内容边界的时候, 该方法可以使用统一的标准的行为来滚动 view。overScrollBy各个参数解释如下:
/** * 当滑动的超出上,下,左,右最大范围时回调 * @param deltaX x方向的瞬时偏移量,左边到头,向右拉为负,右边到头,向左拉为正 * @param deltaY y方向的瞬时偏移量,顶部到头,向下拉为负,底部到头,向上拉为正 * @param scrollX 水平方向的永久偏移量 * @param scrollY 竖直方向的永久偏移量 * @param scrollRangeX 水平方向滑动的范围 * @param scrollRangeY 竖直方向滑动的范围 * @param maxOverScrollX 水平方向最大滑动范围 * @param maxOverScrollY 竖直方向最大滑动范围 * @param isTouchEvent 是否是手指触摸滑动, true为手指, false为惯性 * @return */ @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY,isTouchEvent); }
通过覆盖该方法,就可以达到阻尼回弹的效果。
public class ReboundScrollView extends ScrollView{ private static final int MAX_SCROLL = 200; private static final float SCROLL_RATIO = 0.5f;// 阻尼系数 public ReboundScrollView(Context context) { super(context); } public ReboundScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public ReboundScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { int newDeltaY = deltaY; int delta = (int) (deltaY * SCROLL_RATIO); if((scrollY+deltaY)==0 || (scrollY-scrollRangeY+deltaY)==0){ newDeltaY = deltaY; //回弹最后一次滚动,复位 }else{ newDeltaY = delta; //增加阻尼效果 } return super.overScrollBy(deltaX, newDeltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, MAX_SCROLL, isTouchEvent); } }
public class ReboundHScrollView extends HorizontalScrollView{ private static final int MAX_SCROLL = 200; private static final float SCROLL_RATIO = 0.5f;// 阻尼系数 public ReboundHScrollView(Context context) { super(context); } public ReboundHScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public ReboundHScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { int newDeltaX = deltaX; int delta = (int) (deltaX * SCROLL_RATIO); if((scrollX+deltaX)==0 || (scrollX-scrollRangeX+deltaX)==0){ newDeltaX = deltaX; //回弹最后一次滚动,复位 }else{ newDeltaX = delta; //增加阻尼效果 } return super.overScrollBy(newDeltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, MAX_SCROLL, maxOverScrollY, isTouchEvent); } }
public class ReboundEffectsView extends FrameLayout { private View mPrinceView;// 太子View private int mInitTop, mInitBottom, mInitLeft, mInitRight;// 太子View初始时上下坐标位置(相对父View, // 即当前ReboundEffectsView) private boolean isEndwiseSlide;// 是否纵向滑动 private float mVariableY;// 手指上下滑动Y坐标变化前的Y坐标值 private float mVariableX;// 手指上下滑动X坐标变化前的X坐标值 private int orientation;//1:竖向滚动 2:横向滚动 public ReboundEffectsView(Context context) { this(context, null); } public ReboundEffectsView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ReboundEffectsView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.setClickable(true); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ReboundEffectsView); orientation = ta.getInt(R.styleable.ReboundEffectsView_orientation, 1); ta.recycle(); } /** * Touch事件 */ @Override public boolean onTouchEvent(MotionEvent e) { if (null != mPrinceView) { switch (e.getAction()) { case MotionEvent.ACTION_DOWN: onActionDown(e); break; case MotionEvent.ACTION_MOVE: return onActionMove(e); case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: onActionUp(e);// 当ACTION_UP一样处理 break; } } return super.onTouchEvent(e); } /** * 手指按下事件 */ private void onActionDown(MotionEvent e) { mVariableY = e.getY(); mVariableX = e.getX(); /** * 保存mPrinceView的初始上下高度位置 */ mInitTop = mPrinceView.getTop(); mInitBottom = mPrinceView.getBottom(); mInitLeft = mPrinceView.getLeft(); mInitRight = mPrinceView.getRight(); } /** * 手指滑动事件 */ private boolean onActionMove(MotionEvent e) { float nowY = e.getY(); float diffY = (nowY - mVariableY) / 2; if (orientation == 1 && Math.abs(diffY) > 0) { // 上下滑动 // 移动太子View的上下位置 mPrinceView.layout(mPrinceView.getLeft(), mPrinceView.getTop() + (int) diffY, mPrinceView.getRight(), mPrinceView.getBottom() + (int) diffY); mVariableY = nowY; isEndwiseSlide = true; return true;// 消费touch事件 } float nowX = e.getX(); float diffX = (nowX - mVariableX) / 5;//除数越大可以滑动的距离越短 if (orientation == 2 && Math.abs(diffX) > 0) { // 左右滑动 // 移动太子View的左右位置 mPrinceView.layout(mPrinceView.getLeft() + (int) diffX, mPrinceView.getTop(), mPrinceView.getRight() + (int) diffX, mPrinceView.getBottom()); mVariableX = nowX; isEndwiseSlide = true; return true;// 消费touch事件 } return super.onTouchEvent(e); } /** * 手指释放事件 */ private void onActionUp(MotionEvent e) { if (isEndwiseSlide) { // 是否为纵向滑动事件 // 是纵向滑动事件,需要给太子View重置位置 if (orientation==1){ resetPrinceViewV(); }else if (orientation==2){ resetPrinceViewH(); } isEndwiseSlide = false; } } /** * 回弹,重置太子View初始的位置 */ private void resetPrinceViewV() { TranslateAnimation ta = new TranslateAnimation(0, 0, mPrinceView.getTop() - mInitTop, 0); ta.setDuration(600); mPrinceView.startAnimation(ta); mPrinceView.layout(mPrinceView.getLeft(), mInitTop, mPrinceView.getRight(), mInitBottom); } private void resetPrinceViewH() { TranslateAnimation ta = new TranslateAnimation(mPrinceView.getLeft() - mInitLeft, 0, 0, 0); ta.setDuration(600); mPrinceView.startAnimation(ta); mPrinceView.layout(mInitLeft, mPrinceView.getTop(), mInitRight, mPrinceView.getBottom()); } /** * XML布局完成加载 */ @Override protected void onFinishInflate() { super.onFinishInflate(); if (getChildCount() > 0) { mPrinceView = getChildAt(0);// 获得子View,太子View } }}
attr.xml
layout.xml
Android5.0后Google为Android的滑动机制提供了NestedScrolling特性,可以使我们对嵌套View更简单事件拦截处理。
具体实现思路就是:
给RecyclerView添加头和尾,并控制滑动距离然后通过NestedScrolling进行分发,最后在手指离开界面开启还原动画。NestedScrolling与传统事件分发机制作对比:
NestedScrolling是support.v4提供的支持类,在老版本也可以很好的兼容。
通过NestedScrolling可以实现哪些效果?
实现嵌套分发主要通过以下两个接口:
NestedScrollingParent NestedScrollingChild要使用 NestedScrolling机制,父View 需要实现NestedScrollingParent接口,而子View需要实现NestedScrollingChild接口。RecyclerView已经默认实现了NestedScrollingChild,如果RecyclerView不满足你的业务需求,那需要去实现NestedScrollingChild,这里我只对NestedScrollingParent实现。
public class ReboundLayout extends LinearLayout implements NestedScrollingParent { private View mHeaderView; private View mFooterView; private static final int MAX_WIDTH = 200; private View mChildView; // 解决多点触控问题 private boolean isRunAnim; private int mDrag = 5;//除数越大可以滑动的距离越短 public ReboundLayout(Context context, AttributeSet attrs) { super(context, attrs); setOrientation(LinearLayout.HORIZONTAL); mHeaderView = new View(context); mHeaderView.setBackgroundColor(0xfff); mFooterView = new View(context); mFooterView.setBackgroundColor(0xfff); } @Override protected void onFinishInflate() { super.onFinishInflate(); mChildView = getChildAt(0); LayoutParams layoutParams = new LayoutParams(MAX_WIDTH, LayoutParams.MATCH_PARENT); addView(mHeaderView, 0, layoutParams); addView(mFooterView, getChildCount(), layoutParams); // 左移 scrollBy(MAX_WIDTH, 0); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); ViewGroup.LayoutParams params = mChildView.getLayoutParams(); params.width = getMeasuredWidth(); } /** * 必须要复写 onStartNestedScroll后调用 */ @Override public void onNestedScrollAccepted(View child, View target, int axes) { } /** * 返回true代表处理本次事件 */ @Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { if (target instanceof RecyclerView && !isRunAnim) { return true; } return false; } /** * 复位初始位置 */ @Override public void onStopNestedScroll(View target) { startAnimation(new ProgressAnimation()); } /** * 回弹动画 */ private class ProgressAnimation extends Animation { // 预留 private float startProgress = 0; private float endProgress = 1; private ProgressAnimation() { isRunAnim = true; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { float progress = ((endProgress - startProgress) * interpolatedTime) + startProgress; scrollBy((int) ((MAX_WIDTH - getScrollX()) * progress), 0); if (progress == 1) { isRunAnim = false; } } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); setDuration(260); setInterpolator(new AccelerateInterpolator()); } } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { } @Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { // 如果在自定义ViewGroup之上还有父View交给我来处理 getParent().requestDisallowInterceptTouchEvent(true); // dx>0 往左滑动 dx<0往右滑动 boolean hiddenLeft = dx > 0 && getScrollX() < MAX_WIDTH && !ViewCompat .canScrollHorizontally(target, -1); boolean showLeft = dx < 0 && !ViewCompat.canScrollHorizontally(target, -1); boolean hiddenRight = dx < 0 && getScrollX() > MAX_WIDTH && !ViewCompat .canScrollHorizontally(target, 1); boolean showRight = dx > 0 && !ViewCompat.canScrollHorizontally(target, 1); if (hiddenLeft || showLeft || hiddenRight || showRight) { scrollBy(dx / mDrag, 0); consumed[0] = dx; } // 限制错位问题 if (dx > 0 && getScrollX() > MAX_WIDTH && !ViewCompat.canScrollHorizontally(target, -1)) { scrollTo(MAX_WIDTH, 0); } if (dx < 0 && getScrollX() < MAX_WIDTH && !ViewCompat.canScrollHorizontally(target, 1)) { scrollTo(MAX_WIDTH, 0); } } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { // 当RecyclerView在界面之内交给它自己惯性滑动 if (getScrollX() == MAX_WIDTH) { return false; } return true; } @Override public int getNestedScrollAxes() { return 0; } /** * 限制滑动 移动x轴不能超出最大范围 */ @Override public void scrollTo(int x, int y) { if (x < 0) { x = 0; } else if (x > MAX_WIDTH * 2) { x = MAX_WIDTH * 2; } super.scrollTo(x, y); }}
这里大家可以仿照方法二,添加一个自定义属性,这样就可以灵活的定制回弹动画的方向了。
1、当Recyclerview嵌套方法一控件实现回弹动画时,会导致ScrollView和Recyclerview的嵌套显示不全和滑动问题,此时只需要按照解决这两个View的嵌套使用问题的常用方法解决即可:在Recyclerview的外面嵌套一层RelativeLayout即可解决;
但是,还有有一个问题:当你调用Recyclerview的smoothScrollTo(int position)方法时,这时滚动失效,目前没有找到解决方法,衰。。。望解决过次问题的大神告知,多谢。
2、当Recyclerview嵌套方法二控件实现回弹动画时,没有回弹效果,如果修改代码的话bug太多,所以暂时没有解决此问题;其他控件都可正常使用没有问题;
3、当Recyclerview嵌套方法三控件实现回弹动画时,就可以完美解决方法一种的所有问题啦,(/≧▽≦)/,所以如果你想给Recyclerview添加阻尼回弹动画建议用方法三哦。
参考文章:
1、 2、转载地址:http://dgcqf.baihongyu.com/