YCShopDetailLayout

Introduction: 购物商场商品详情页面自定义控件,模仿淘宝、京东、考拉等商品详情页分页加载的 UI 效果。可以嵌套 RecyclerView、WebView、ViewPager、ScrollView 等等。支持设置上拉分页加载动画效果……
More: Author   ReportBugs   
Tags:

目录介绍

  • 01.该库介绍
  • 02.效果展示
  • 03.如何使用
  • 04.注意要点
  • 05.优化问题
  • 06.部分代码逻辑
  • 07.参考案例

01.该库介绍

02.效果展示

  • slide.gif

2.1 使用 SlideLayout 效果

  • image

2.2 使用 SlideAnimLayout 带有加载动画效果

  • image
  • image

03.如何使用

3.0 如何引入到项目中

  • 如下所示
      implementation 'com.github.yangchong211:YCSlideLayout:1.1.6'
    

3.1 第一种,直接上拉加载分页【SlideLayout 有两个子 ChildView】

  • SlideDetailsLayout 有两个子 ChildView:一个是商品页 layout,一个是详情页 layout
  • 在布局中 ```

      <!--商品布局-->
      <FrameLayout
          android:id="@+id/fl_shop_main"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"/>
    
      <!--分页详情 webView 布局-->
      <include layout="@layout/include_shop_detail"/>
    
</com.ycbjie.slide.SlideLayout>
```
  • 在代码中

      mSlideDetailsLayout.setOnSlideDetailsListener(new SlideLayout.OnSlideDetailsListener() {
          @Override
          public void onStatusChanged(SlideLayout.Status status) {
              if (status == SlideLayout.Status.OPEN) {
                  //当前为图文详情页
                  Log.e("FirstActivity","下拉回到商品详情");
              } else {
                  //当前为商品详情页
                  Log.e("FirstActivity","继续上拉,查看图文详情");
              }
          }
      });
    
      //关闭商详页
      mSlideDetailsLayout.smoothClose(true);
      //打开详情页
      mSlideDetailsLayout.smoothOpen(true);
    

3.2 第一种,上拉加载有动画效果,然后展示分页【SlideAnimLayout 有三个子 ChildView】

  • SlideAnimLayout 有三个子 ChildView:一个是商品页 layout,一个是上拉加载动画 layout,一个是详情页 layout
  • 在布局中 ```

     <com.ycbjie.slide.SlideAnimLayout
          android:id="@+id/slideDetailsLayout"
          android:layout_width="match_parent"
          android:layout_height="0dp"
          android:layout_weight="1"
          app:default_panel="front"
          app:duration="200"
          app:percent="0.1">
    
          <!--商品布局-->
          <FrameLayout
              android:id="@+id/fl_shop_main2"
              android:layout_width="match_parent"
              android:layout_height="match_parent"/>
    
          <!--上拉加载动画布局-->
          <LinearLayout
              android:id="@+id/ll_page_more"
              android:orientation="vertical"
              android:background="@color/colorAccent"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">
              <ImageView
                  android:id="@+id/iv_more_img"
                  android:layout_width="40dp"
                  android:layout_height="40dp"
                  android:rotation="180"
                  android:layout_gravity="center_horizontal"
                  android:src="@mipmap/icon_details_page_down_loading" />
              <TextView
                  android:id="@+id/tv_more_text"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_gravity="center_horizontal"
                  android:layout_marginBottom="25dp"
                  android:gravity="center"
                  android:text="测试动画,继续上拉,查看图文详情"
                  android:textSize="13sp" />
          </LinearLayout>
    
          <!--分页详情 webView 布局-->
          <include layout="@layout/include_shop_detail"/>
    
    </com.ycbjie.slide.SlideAnimLayout>
```
  • 在代码中

      mSlideDetailsLayout.setScrollStatusListener(new SlideAnimLayout.onScrollStatusListener() {
          @Override
          public void onStatusChanged(SlideAnimLayout.Status mNowStatus, boolean isHalf) {
              if(mNowStatus== SlideAnimLayout.Status.CLOSE){
                  //打开
                  if(isHalf){
                      mTvMoreText.setText("释放,查看图文详情");
                      mIvMoreImg.animate().rotation(0);
                      LoggerUtils.i("onStatusChanged---CLOSE---释放"+isHalf);
                  }else{//关闭
                      mTvMoreText.setText("继续上拉,查看图文详情");
                      mIvMoreImg.animate().rotation(180);
                      LoggerUtils.i("onStatusChanged---CLOSE---继续上拉"+isHalf);
                  }
              }else{
                  //打开
                  if(isHalf){
                      mTvMoreText.setText("下拉回到商品详情");
                      mIvMoreImg.animate().rotation(0);
                      LoggerUtils.i("onStatusChanged---OPEN---下拉回到商品详情"+isHalf);
                  }else{//关闭
                      mTvMoreText.setText("释放回到商品详情");
                      mIvMoreImg.animate().rotation(180);
                      LoggerUtils.i("onStatusChanged---OPEN---释放回到商品详情"+isHalf);
                  }
              }
          }
      });
    
      //关闭商详页
      mSlideDetailsLayout.smoothClose(true);
      //打开详情页
      mSlideDetailsLayout.smoothOpen(true);
    

04.注意要点

  • 针对 SlideDetailsLayout 仅获取子节点中的前两个 View
    • 其中第一个作为 Front,即商品页;第二个作为 Behind,即图文详情 WebView 页面。具体看代码:
      @Override
      protected void onFinishInflate() {
        super.onFinishInflate();
        final int childCount = getChildCount();
        if (1 >= childCount) {
            throw new RuntimeException("SlideDetailsLayout only accept child more than 1!!");
        }
        mFrontView = getChildAt(0);
        mBehindView = getChildAt(1);
        if(mDefaultPanel == 1){
            post(new Runnable() {
                @Override
                public void run() {
                    //默认是关闭状态的
                    smoothOpen(false);
                }
            });
        }
      }
      
  • 针对 SlideAnimLayout 仅获取子节点中三个 View,且第二个为动画节点 View
    • 其中第一个作为 Front,即商品页;第二个作为 anim,即上拉动画 view。第三个作为 Behind,即图文详情 WebView 页面。具体看代码:
      @Override
      protected void onFinishInflate() {
        super.onFinishInflate();
        final int childCount = getChildCount();
        if (1 >= childCount) {
            throw new RuntimeException("SlideDetailsLayout only accept childs more than 1!!");
        }
        mFrontView = getChildAt(0);
        mAnimView = getChildAt(1);
        mBehindView = getChildAt(2);
        mAnimView.post(new Runnable() {
            @Override
            public void run() {
                animHeight = mAnimView.getHeight();
                LoggerUtils.i("获取控件高度"+animHeight);
            }
        });
        if(mDefaultPanel == 1){
            post(new Runnable() {
                @Override
                public void run() {
                    //默认是关闭状态的
                    smoothOpen(false);
                }
            });
        }
      }
      

05.优化问题

  • 异常情况保存状态

      @Override
      protected Parcelable onSaveInstanceState() {
          SavedState ss = new SavedState(super.onSaveInstanceState());
          ss.offset = mSlideOffset;
          ss.status = mStatus.ordinal();
          return ss;
      }
    
      @Override
      protected void onRestoreInstanceState(Parcelable state) {
          SavedState ss = (SavedState) state;
          super.onRestoreInstanceState(ss.getSuperState());
          mSlideOffset = ss.offset;
          mStatus = Status.valueOf(ss.status);
          if (mStatus == Status.OPEN) {
              mBehindView.setVisibility(VISIBLE);
          }
          requestLayout();
      }
    
  • 当页面销毁的时候,移除 listener 监听,移除动画资源
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          setScrollStatusListener(null);
          setOnSlideStatusListener(null);
          if (animator!=null){
              animator.cancel();
              animator = null;
          }
      }
    

06.部分代码逻辑

6.1 如何实现 ScrollView 在最顶部或者最底部的时候,不消费事件

  • 具体逻辑在 dispatchTouchEvent 分发事件中,当滑动到顶部或者底部的时候,则直接让父 View 消费事件。其他情况是自己是将事件会向上返还给 View 的父节点。
      @Override
      public boolean dispatchTouchEvent(MotionEvent ev) {
          switch (ev.getAction()) {
              case MotionEvent.ACTION_DOWN:
                  downX = ev.getX();
                  downY = ev.getY();
                  //如果滑动到了最底部,就允许继续向上滑动加载下一页,否者不允许
                  //如果子节点不希望父进程拦截触摸事件,则为 true。
                  getParent().requestDisallowInterceptTouchEvent(true);
                  break;
              case MotionEvent.ACTION_MOVE:
                  float dx = ev.getX() - downX;
                  float dy = ev.getY() - downY;
                  boolean allowParentTouchEvent;
                  if (Math.abs(dy) > Math.abs(dx)) {
                      if (dy > 0) {
                          //位于顶部时下拉,让父 View 消费事件
                          allowParentTouchEvent = isTop();
                      } else {
                          //位于底部时上拉,让父 View 消费事件
                          allowParentTouchEvent = isBottom();
                      }
                  } else {
                      //水平方向滑动
                      allowParentTouchEvent = true;
                  }
                  getParent().requestDisallowInterceptTouchEvent(!allowParentTouchEvent);
                  break;
              default:
                  break;
          }
          return super.dispatchTouchEvent(ev);
      }
    

6.2 如何实现商品页和详情页之间的滑动,如何处理上拉加载控件的动画效果

  • SlideAnimLayout 有三个子 ChildView:一个是商品页 layout,一个是上拉加载动画 layout,一个是详情页 layout
  • 通过 onInterceptTouchEvent 进行事件拦截后,在 onTouchEvent 方法中对触摸信息做进一步处理可以实现竖直方向的滑动
    • 当商品页 ScrollView 滑动到底部时,则直接让父 View 消费事件,该父 View 也就是 SlideAnimLayout
    • 在 onInterceptTouchEvent 中,当打开详情页后(也就是 CLOSE 状态),向下拉动,当 y 轴滑动位移绝对值大于触摸移动的像素距离,并且当 y 轴滑动位移大于 0,则拦截事件分发自己消费事件
    • 在 onInterceptTouchEvent 中,当关闭详情页后(也就是 OPEN 状态),向上拉动,当 y 轴滑动位移绝对值大于触摸移动的像素距离,并且当 y 轴滑动位移小于 0,则拦截事件分发自己消费事件
    • 当处在商品页时,向上拉动;或者处于详情页时,向下拉动,在拉动过程中去改变 mSlideOffset 值,并且调用 requestLayout()方法去绘制
    • 在屏幕区域滑动两个面板只需要改变两个面板在 y 轴方向的位移(有正负方向)即可。滑动的标尺是控件相对于 Top 的移动,且所有的位移计算都是基于该标尺。在切换面板时只需要知道对应的 offset 值即可……
  • 如何处理上拉加载控件的动画效果
    • 添加一个 listener 监听,可以监听到状态,以及是否达到一半距离,主要是和 offset 比较,当到达一半距离的时候,这个时候用属性动画将箭头 view 旋转 180 度即可实现。
    • 既然要监听滑动距离,则首先要获取该加载控件的高度 animHeight,那么在哪里获取比较合适呢?可以在 onFinishInflate()方法中,用 post 形式获取控件高度。
  • 那么如何使滑动生效,并且看上去比较连贯
    • 自定义布局中有非常重要的两个环节 onMeasure(测量)和 onLayout(布局)。测量决定了 View 的所占的大小,布局决定了 View 所处的位置。实现滑动的关键思路就在这里,我们在 onLayout 方法中根据通过 onInterceptTouchEvent、onTouchEvent 得到的滑动信息进行计算而得到布局的位置信息,并把这个位置信息设置到子 View 上面即可实现滑动。
  • 滑动后松开手指如何实现滚动效果
    • 也就是说,当处在商品页时,向上拉动,拉动位移大于一半时,松开手指,则直接滑动到下一页详情页页面
    • 具体逻辑在 finishTouchEvent 方法中,它主要是记录 offset 值,以及 close 或 open 状态下视图的高度,还有是否发生切换变化
    • 最后开启动画,在动画过程中添加动画 update 的监听,在该方法中去 requestLayout()控件,这样就达到滚动效果了。动画滚动结束后,如果是 open 状态并且是第一次显示,则设置详情页控件可见。
  • 如何使滚动效果比较自然,或者如何调整滚动时长
    • 可以自定义设置时间,直接在布局中设置……

07.参考案例

08.其他更多

01.关于博客汇总链接

02.关于我的博客

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools