欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Android添加自定义下拉刷新布局阻尼滑动悬停弹动画效果,

来源: 开发者 投稿于  被查看 15321 次 评论:198

Android添加自定义下拉刷新布局阻尼滑动悬停弹动画效果,


目录
  • Android 对现有布局添加下拉刷新
  • 一、简述
    • 1、下拉阶段
    • 2、下拉松手阶段
  • 二、现有布局
    • 三、添加下拉刷新
      • 1、一个响应下拉操作的父容器控件
        • (1)onInterceptTouchEvent
        • (2)onTouchEvent
      • 2、下拉刷新头部区域
        • 3、将下拉刷新头部 及 内容区域 引入到 响应下拉操作的父容器控件中
          • 4、回弹悬停动画
            • 5、回弹到顶部的动画
              • 6、在某些时机下,进行回调
              • 四、遇到的问题
                • 如何解决呢

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

                  先直接上效果,如下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、将下拉刷新头部 及 内容区域 引入到 响应下拉操作的父容器控件中

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

                  <?xml version="1.0" encoding="utf-8"?>
                  <com.qlli.pulllayout.PullRefreshLayout
                      android:id="@+id/pull_layout"
                      xmlns:android="http://schemas.android.com/apk/res/android"
                      xmlns:app="http://schemas.android.com/apk/res-auto"
                      xmlns:tools="http://schemas.android.com/tools"
                      android:layout_width="match_parent"
                      android:layout_height="match_parent"
                      tools:context=".MainActivity"
                      android:background="@color/teal_700">
                        <com.qlli.pulllayout.PullRefreshHeadView
                            android:id="@+id/pull_header"
                            android:layout_width="match_parent"
                            android:layout_height="@dimen/dp54"
                            android:background="@color/red"/>
                        <RelativeLayout
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:background="@color/color_415fff"
                            android:gravity="center"
                            android:clickable="true">
                              <TextView
                                  android:layout_width="wrap_content"
                                  android:layout_height="wrap_content"
                                  android:textColor="@color/white"
                                  android:textSize="20sp"
                                  android:text="这里是内容区域, 下拉试试看"/>
                        </RelativeLayout>
                  </com.qlli.pulllayout.PullRefreshLayout>
                  

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

                  @Override
                  protected void onFinishInflate() {
                      super.onFinishInflate();
                      ensureTargetView();
                  }
                  //寻找需要控制滑动的内容区域的父容器
                  private void ensureTargetView() {
                      if (mTarget != null || getChildCount() <= 0) {
                          return;
                      }
                      for (int index = 0; index < getChildCount(); index++) {
                          View child = getChildAt(index);
                          if (child instanceof PullRefreshHeadView) {
                              mPullRefreshHeadView = (PullRefreshHeadView) child;
                              continue;
                          }
                          if (child != mPullRefreshHeadView) {
                              mTarget = child;
                              break;
                          }
                      }
                  }
                  

                  4、回弹悬停动画

                  回弹悬停动画是指:先回弹到指定位置,然后开始悬停一段时间后,再开启一个新的动画

                  回弹动作:是指将 下拉刷新头部 及 内容区域 回弹至指定位置,可以在一个时间段中,通过监听0到100变化的,进而动态计算改变下拉刷新头部及内容区域的高度并更新

                  悬停动作:在回弹结束后,其实此时悬停是指回弹动画结束后,就保持当前位置不动了,此时使用Handler发一个延时任务去执行 一个新的回弹动画(将下拉刷新及内容区域回弹至原始位置),这个中间的过程给出的视觉效果是一个悬停的效果

                  private ValueAnimator mHoverAnimator;//回弹悬停动画
                  private final Handler mHoverHandler = new Handler(Looper.getMainLooper());
                  private void animateToHover() {
                      // 这里是内容区域marginTop的距离
                      final int startPosition = mOffsetTop;
                      // 这里是动画结束的位置,要保留一个下拉刷新头部高度距离
                      final int totalDistance = startPosition - mDp54;
                      // 设置悬停动画的一些初始化东西
                      if (mHoverAnimator == null) {
                          mHoverAnimator = ValueAnimator.ofFloat(0f, 100f);
                          mHoverAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
                      } else {
                          mHoverAnimator.removeAllUpdateListeners();
                          mHoverAnimator.removeAllListeners();
                          mHoverAnimator.end();
                      }
                      // 在动画监听过程中,通过updateHeightAndMargin移动下拉刷新及内容区域的距离
                      mHoverAnimator.addUpdateListener(animation -> {
                          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;
                  }
                  

                  如何解决呢

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

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

                   <RelativeLayout
                            android:layout_width="match_parent"
                            android:layout_height="match_parent"
                            android:background="@color/color_415fff"
                            android:gravity="center"
                            android:clickable="true">

                  以上就是Android添加自定义下拉刷新布局阻尼滑动悬停弹动画效果的详细内容,更多关于Android添加下拉刷新布局的资料请关注3672js教程其它相关文章!

                  您可能感兴趣的文章:
                  • Android布局控件View ViewRootImpl WindowManagerService关系
                  • Android进阶CoordinatorLayout协调者布局实现吸顶效果
                  • Android嵌套线性布局玩法坑解决方法
                  • Android布局ConstraintLayout代码修改约束及辅助功能
                  • Android修行手册之ConstraintLayout布局使用详解
                  • Android使用ViewStub实现布局优化方法示例

                  用户评论