Android:对现有布局添加自定义的下拉刷新布局(阻尼滑动、悬停、回弹动画效果) 您所在的位置:网站首页 mac怎么刷新界面 Android:对现有布局添加自定义的下拉刷新布局(阻尼滑动、悬停、回弹动画效果)

Android:对现有布局添加自定义的下拉刷新布局(阻尼滑动、悬停、回弹动画效果)

2023-03-11 14:39| 来源: 网络整理| 查看: 265

Android 对现有布局添加下拉刷新

先直接上效果,如下GIF所示

下拉刷新-效果展示.gif

一、简述

对现有布局添加一个下拉刷新,并且这个动画的效果如上GIF所示

1、下拉阶段

下拉过程中,有阻尼滑动效果

2、下拉松手阶段 (1)、进行高度判断,若大于指定的高度后,先回弹到指定的高度后,做悬停动画效果,再然后做回弹动画回弹到原始位置 (2)、若没有大于指定的高度,则直接回弹到原始位置 (3)刷新的时机,可以自由选择,例如在松手时,即发起刷新逻辑。 复制代码 二、现有布局

如前面的GIF所示,蓝色区域是内容区域,即是添加下拉刷新前的现有布局

三、添加下拉刷新

从GIF图可以看出,添加下拉刷新,需要两个控件:一个响应下拉操作的父容器控件、一个是刷新头部控件

下拉刷新的主要思路:

页面布局:将响应下拉操作的父容器控件包裹红色下拉刷新头部区域 和 蓝色内容区域,其中蓝色内容区域覆盖在红色下拉刷新头部区域的上面。

下拉操作:下拉时,动态地改变红色下拉刷新头部区域的高度,以及动态改变蓝色内容区域的marginTop值

然后,就是动画操作,也是动态地改变红色下拉刷新头部区域的高度 和 蓝色内容区域的marginTop值。

1、一个响应下拉操作的父容器控件

为写起来简单,直接继承RelativeLayout,重点重写onInterceptTouchEvent 和 onTouchEvent方法。

(1)onInterceptTouchEvent

拦截事件方法:

首先,判断该事件是否需要拦截;

然后,若拦截该事件:在down事件时,将之前操作红色下拉刷新头部区域 及 蓝色内容区域都重置下

然后,在move事件时,判断当前移动的距离是否 > mTouchSlop(表示滑动的最小距离) ,当大于时,认为此时产生了拖拽滑动

最后,在up\cancel事件时,将拖拽标志 重置回来

@Override public boolean onInterceptTouchEvent(MotionEvent event) { if (不拦截事件的判断条件) { return false; } if (若此时正在执行动画,则拦截该事件) { return true; } final int action = event.getActionMasked();//获取触控手势 switch (action) { case MotionEvent.ACTION_DOWN: // 重置操作 updateHeightAndMargin(0); mIsDragging = false; // 手指按下的距离 this.mDownY = event.getY(); break; case MotionEvent.ACTION_MOVE: final float y = event.getY(); final float yDiff = y - this.mDownY; if (yDiff > mTouchSlop) { //判断是否时产生了拖拽 mIsDragging = true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIsDragging = false; break; default: break; } return mIsDragging; } 复制代码 (2)onTouchEvent

触摸事件处理方法:

若此时没有发生拖拽,或者此时正在动画中: 不处理该事件

当在move事件时:计算阻尼滑动距离,然后更新给红色的下拉刷新头部区域 及 蓝色的内容区域

当在up/cancel事件时: 开启动画逻辑

@SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { if (!mIsDragging || mIsAnimation) { return super.onTouchEvent(event); } //获取触控手势 final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_MOVE: { //获取移动距离 float eventY = event.getY(); float yDiff = eventY - mDownY; float scrollTop = yDiff * 0.5; //计算实际需要被拖拽产生的移动百分比 mDragPercent = scrollTop / mDp330; if (mDragPercent < 0) { return false; } //计算阻尼滑动的距离 int targetY = (int) (computeTargetY(scrollTop, mDragPercent, mDp330) + 0.5f); updateHeightAndMargin(targetY); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { final float upDiffY = event.getY() - mDownY; final float overScrollTop = upDiffY * DEFAULT_DRAG_RATE; mIsDragging = false; if (overScrollTop > mDp54) { animateToHover(); } else { animateToPeak(); } mExtraDrag = 0; mPullRefreshBehavior.onUp(); return false; } default: break; } return true; } 复制代码

阻尼滑动的计算方式:

/*计算阻尼滑动距离*/ public int computeTargetY(float scrollTop, float dragPercent, float maxDragDistance) { float boundedDragPercent = Math.min(1.0f, Math.abs(dragPercent)); float extraOS = Math.abs(scrollTop) - maxDragDistance; float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, maxDragDistance * 2) / maxDragDistance); float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f; float extraMove = (maxDragDistance) * tensionPercent / 2; return (int) ((maxDragDistance * boundedDragPercent) + extraMove); } 复制代码

更新红色头部区域(mPullRefreshHeadView)高度 及 蓝色的内容区域(mTarget)

private void updateHeightAndMargin(int offsetTop) { if (mPullRefreshHeadView == null || mTarget == null) { return; } // 更新下拉刷新的头部高度 ViewGroup.LayoutParams headViewLayoutParams = mPullRefreshHeadView.getLayoutParams(); if (headViewLayoutParams != null) { headViewLayoutParams.height = Math.max(offsetTop, mDp54); } // 更新 mTarget view 的 topMargin MarginLayoutParams targetLayoutParams = (MarginLayoutParams) mTarget.getLayoutParams(); if (targetLayoutParams != null) { targetLayoutParams.topMargin = offsetTop; } mOffsetTop = offsetTop; mPullRefreshBehavior.onMove(mOffsetTop); // 刷新界面 requestLayout(); } 复制代码 2、下拉刷新头部区域

这里可以根据自己的需求去构建下拉刷新头部区域的布局,例如添加Lottie动画等

代码示例,是比较简单的一个 Textview + 背景展示下

public class PullRefreshHeadView extends RelativeLayout { private View mHeaderView; public PullRefreshHeadView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { Resources resources = context.getResources(); mHeaderView = LayoutInflater.from(context).inflate(R.layout.vivoshop_classify_pull_refresh_head, this, false); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, context.getResources().getDimensionPixelSize(R.dimen.dp54)); params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); params.addRule(RelativeLayout.CENTER_HORIZONTAL); params.bottomMargin = resources.getDimensionPixelSize(R.dimen.dp9); addView(mHeaderView, params); } } 复制代码 3、将下拉刷新头部 及 内容区域 引入到 响应下拉操作的父容器控件中

布局:响应下拉操作的父容器控件包裹着下拉刷新头部及内容区域

复制代码

在响应下拉操作的父容器控件初始化时,在onFinishInflate中将下拉刷新头部、内容区域分别进行赋值

@Override protected void onFinishInflate() { super.onFinishInflate(); ensureTargetView(); } //寻找需要控制滑动的内容区域的父容器 private void ensureTargetView() { if (mTarget != null || getChildCount() { Object value = animation.getAnimatedValue(); if (value instanceof Float) { float percent = ((float) value) / 100f; int targetTop = startPosition - (int) (totalDistance * percent); updateHeightAndMargin(targetTop); } }); // 监听此动画开始 和 结束点 mHoverAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mIsAnimation = true; } // 在该动画结束后,在1.6s后,做一个回弹动画,因此在1.6s的时间内就是一个悬停效果 // 可以在这个悬停的期间干些事情,例如播放Lottie动画等 @Override public void onAnimationEnd(Animator animation) { mHoverHandler.removeCallbacksAndMessages(null); mHoverHandler.postDelayed(() -> { if (isAttachedToWindow()) { // 例如在这个播放Lottie动画 ensureTargetView(); // 回弹动画 animateToPeak(); } }, 1600); } }); // 此动画设置一下时间 float animationPercent = Math.min(1.0f, Math.abs(totalDistance) * 1.0f / mDp54); long duration = Math.abs((long) (ANIMATION_DURATION_300 * animationPercent)); mHoverAnimator.setDuration(duration); mHoverAnimator.start(); } 复制代码 5、回弹到顶部的动画

这个回弹到顶部的操作是指:将下拉刷新头部 及 内容区域 在一定时间内 回到顶部

private ValueAnimator mPeakAnimator;//回弹动画 private void animateToPeak() { float startDragPercent = mDragPercent; //松手后开始从此位置滑动 final int totalDistance = mOffsetTop; if (mPeakAnimator == null) { mPeakAnimator = ValueAnimator.ofFloat(0f, 100f); mPeakAnimator.setInterpolator(new DecelerateInterpolator(2.0f)); } else { mPeakAnimator.removeAllListeners(); mPeakAnimator.removeAllUpdateListeners(); mPeakAnimator.end(); } mPeakAnimator.addUpdateListener(animation -> { Object value = animation.getAnimatedValue(); if (value instanceof Float) { float percent = ((float) value) / 100f; int targetTop = (int) (totalDistance * (1.0f - percent)); updateHeightAndMargin(targetTop); } }); mPeakAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mIsAnimation = true; } @Override public void onAnimationEnd(Animator animation) { mIsAnimation = false; updateHeightAndMargin(0); } }); float ratio = Math.abs(startDragPercent); // 滑动到顶部的时间 mPeakAnimator.setDuration((long) (800 * ratio)); mPeakAnimator.start(); } 复制代码 6、在某些时机下,进行回调

可以结合自己的需求写一个接口,例如下面这样:

public interface PullRefreshBehavior { // 移动的高度 void onMove(int height); // 手指抬起 void onUp(); // 悬停 void onHover(); // 回弹 void onSpringBack(); // 完成 void onComplete(); } 复制代码

然后在下拉操作的过程中 去选择性地调用 上面接口中的方法,这样在实现该接口的具体实现类中,就能根据当前下拉操作的不同时机来去做一些想做的事情

四、遇到的问题 1、在下拉操作时,在onInterceptTouchEvent方法时仅响应down事件,move事件不响应

导致该问题的主要原因是:响应下拉操作的父容器内包裹的子控件没有消耗down事件,所以后续收不到move事件

2、看下ViewGroup中的事件分发这段代码

可以看到下面代码中: 是down事件,或者 mFirstTouchTarget != null

若父容器包裹的子控件没有消耗down事件,则mFirstTouchTarget == null,那么当move事件到来是,即不满足条件,则不会调用到 onInterceptTouchEvent方法。

// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } 复制代码 3、如何解决呢

在子控件中,加一个消耗down事件的操作即可,例如在子控件布局中,添加一个clickable属性为 true 即可

因为可点击事件,是消耗down事件的

复制代码


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有