自定义ScrollView和TabLayout联动(一)

本文分享了在项目中实现ScrollView与TabLayout联动的具体过程,包括解决滑动切换标签、惯性滑动及样式同步等问题的方法,通过自定义ScrollView优化用户体验。

最近在做新项目,需要实现一个淘宝商品详情页的效果,根据滚动的距离切换顶部的tab标签,点击tab标签可以滚动到指定位置。第一次看到这个效果想到的实现方案就是ScrollView和tabLayou的联动,但是因为对view的滑动事件不是很了解,很多方法也不知道,所以只能去百度、谷歌了。在查找的过程中发现RecyclerView也可以实现这个效果,想着RecyclerView实现的话,由于每个内容板块都是一个item,滑动定位应该会很准,不过由于多个请求,需要重新组装数据,有感觉很麻烦,放弃了,现在想起来是有点懒。回到ScrollView的实现方案后查到了一个自定义的ScrollView,只需要将每个内容板块的高度计算好后传到ScrollView,由他内部去处理就可以了,感觉使用起来很简单,就选择了他。一顿操作下载终于实现了这个效果,不过当去使用的时候发现好多问题,比如滑动切换标签的时机都是错的,(这个是因为请求数据后,对一些view隐藏显示导致的,后来选择在每次请求数据后都去重新计算高度,不过效果还是有点差),还有当ScrollView处于惯性滑动时,切换标签无法停止惯性滑动,还有tablayout的指示器和选中状态不统一各种问题,当时有点菜一个也没解决。这次新项目再次用到了这个效果,我觉得是时候去研究一下了原理了,就开始看那个自定义的ScrollView尝试去理解它,一个一个的去解决问题。首先看一下实现后的效果:
详情界面.gif

下面是遇到的问题,我们一个一个的去解决:
####1.切换标签的滑动距离计算的不对:
因为项目中这个界面有3个请求,如果按照之前的写法,我需要在3个请求的成功回调中去重新计算高度,还有一些大图,可能无法立即显示,这个高度也无法去准确的计算出来,所以我干脆不再提前计算好,而是直接将内容板块的view传递过去,实时计算view距离顶部的距离,再根据距离顶部的距离去切换tab。

        List<View> views = new LinkedList<>();
        views.add(view1);
        views.add(view2);
        views.add(view3);
        views.add(view4);
        scrollView.setAnchorList(views); // 设置视图集合

重写ScrollView的onScrollChanged方法,去循环views,然后使用**getViewTop()获取view距离顶部的高度,如果滑动距离t大于该view距顶部的高度,就使用setSelectedTab()**切换到该tab标签。

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mViewList == null) {
            return;
        }
        for (int i = mViewList.size() - 1; i >= 0; i--) {
            if (t > getScrollDistance(i)) {
                // 当达到该View的高度时去切换
                setSelectedTab(i);
                break;
            }
        }
    }

    /**
     * 获取View距离顶部的高度(mTranslationY是距离顶部的偏移量)
     *
     * @param position
     * @return
     */
    private int getViewTop(int position) {
        if (position >= mViewList.size() + 1) {
            throw new IndexOutOfBoundsException("TabLayout的tab数量和视图View的数量不一致");
        }
        return mViewList.get(position).getTop() - mTranslationY;
    }

    /**
     * 设置选中的tab标签
     *
     * @param position
     */
    private void setSelectedTab(int position) {
        if (mTabLayout != null && position != oldPosition) {
            // 该方法不会走tabLayout的onTabSelected监听
            mTabLayout.setScrollPosition(position, 0, true);
        }
        oldPosition = position;
    }

这样切换标签时机就非常准确了。
####问题2:惯性滑动时去切换tab无法终止惯性滑动,导致会自己滑动到下个标签。
造成这个问题的原因其实是因为调用方法错误的原因,之前调用的是scrollTo(x, y)方法,改成smoothScrollTo(x, y)就可以了。下面是两个方法的区别

scrollTo(x, y); // 该方法会立即滚动到指定的坐标,不会终止正在滑动的事件。

smoothScrollTo(x, y);// 该方法会平缓的滚动到指定的坐标,并且会终止正在滚动的事件。

####问题3:tab标签的指示器和选择tab的字体颜色错乱
其实造成这个的原因和问题2有关系,在我点击tab切换标签的时候,scrollview又惯性滚动到了下个标签,导致tabLayou没有及时的切换所有的状态造成的,解决了问题2该问题也不再出现。这下问题都解决了,回过头来发现其实问题都很简单,只是当时有点摸不着头脑,而且从来没有去看里面的实现逻辑,就打了退堂鼓觉得自己不能理解,以后要拒绝当伸手党,应该多看看源码,理解里面的原理。

为了更方便的使用,我将tabLayout也传递进去了,这样只需要几行代码就可以实现这个效果了,实现起来就两步
1.第一步就是将内容板块的view集合传递进来

        List<View> views = new LinkedList<>();
        views.add(llCommodity);
        views.add(cardEvaluate);
        views.add(llDetails);
        views.add(llRecommend);
        scrollView.setAnchorList(views); // 设置视图集合

2.第二部绑定tabLayout

scrollView.setupWithTabLayout(tabLayout); // 设置绑定的tabLayout

3.如果还想实现一个标题栏的渐变,可以设置滑动监听和距离顶部的偏差值

// 因为标题栏在ScrollView上面,所以需要设置一个距离顶部的偏差值(标题栏的高度+状态栏的高度),防止标题栏遮盖布局
scrollView.setTranslationY(SizeUtils.getMeasuredHeight(toolbar) + BarUtils.getStatusBarHeight());
scrollView.setOnScrollCallback(new TabWithScrollView.OnScrollCallback() {
            @Override
            public void onScrollCallback(int l, int t, int oldl, int oldt) {
                setBgAlphaChange(t, height);
            }
        });

下面是整个类的代码以及源码

/**
* Created by Hao on 2019/7/21.
* Describe ScrollView和TabLayout的联动
*/
public class TabWithScrollView extends ScrollView {

  private static final String TAG = "TabWithScrollView";

  /**
   * 模块View的集合
   */
  private List<View> mViewList;

  /**
   * 是否是ScrollView引起的滑动,true-是,false-TabLayout引起的滑动
   */
  private boolean isManualScroll;

  /**
   * 记录上一次点击的position,防止多次点击
   */
  private int oldPosition = 0;

  /**
   * 需要联动的tabLayout
   */
  private TabLayout mTabLayout;

  /**
   * ScrollView的滑动回调
   */
  private OnScrollCallback onScrollCallback;

  /**
   * 距离顶部的偏移量,默认为10px;
   */
  private int mTranslationY = 10;


  public TabWithScrollView(Context context) {
      super(context);
      setOnTouchListener();
  }

  public TabWithScrollView(Context context, AttributeSet attrs) {
      super(context, attrs);
      setOnTouchListener();
  }

  public TabWithScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
      super(context, attrs, defStyleAttr);
      setOnTouchListener();
  }

  @SuppressLint("ClickableViewAccessibility")
  public void setOnTouchListener() {
      super.setOnTouchListener(new OnTouchListener() {
          @Override
          public boolean onTouch(View v, MotionEvent event) {
              if (event.getAction() == MotionEvent.ACTION_DOWN) {
                  isManualScroll = true;
              }
              return false;
          }
      });
  }

  @Override
  protected void onScrollChanged(int l, int t, int oldl, int oldt) {
      super.onScrollChanged(l, t, oldl, oldt);
      if (onScrollCallback != null) {
          onScrollCallback.onScrollCallback(l, t, oldl, oldt);
      }
      if (isManualScroll) {
          if (mViewList == null) {
              return;
          }
          for (int i = mViewList.size() - 1; i >= 0; i--) {
              if (t > getViewTop(i)) {
                  setSelectedTab(i);
                  break;
              }
          }
      }
  }

  /**
   * 获取View距离顶部的高度(mTranslationY是距离顶部的偏移量)
   *
   * @param position
   * @return
   */
  private int getViewTop(int position) {
      if (position >= mViewList.size() + 1) {
          throw new IndexOutOfBoundsException("TabLayout的tab数量和视图View的数量不一致");
      }
      return mViewList.get(position).getTop() - mTranslationY;
  }

  /**
   * 设置选中的tab标签
   *
   * @param position
   */
  private void setSelectedTab(int position) {
      if (mTabLayout != null && position != oldPosition) {
          // 该方法不会走tabLayout的onTabSelected监听
          mTabLayout.setScrollPosition(position, 0, true);
      }
      oldPosition = position;
  }

  /**
   * 设置绑定的tabLayout,并给tabLayout添加OnTabSelectedListener监听
   *
   * @param tabLayout
   */
  public void setupWithTabLayout(TabLayout tabLayout) {
      if (mTabLayout != null) {
          mTabLayout.removeOnTabSelectedListener(mTabSelectedListener);
      }
      if (tabLayout != null) {
          mTabLayout = tabLayout;
          mTabLayout.addOnTabSelectedListener(mTabSelectedListener);
      }
  }

  public void setAnchorList(List<View> anchorList) {
      this.mViewList = anchorList;
  }

  public void setOnScrollCallback(OnScrollCallback onScrollCallback) {
      this.onScrollCallback = onScrollCallback;
  }

  public void setTranslationY(int translationY) {
      this.mTranslationY = translationY;
  }

  TabLayout.OnTabSelectedListener mTabSelectedListener = new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
          isManualScroll = false;
          if (mViewList == null) {
              Log.i(TAG, "onTabSelected: 未设置View集合");
              return;
          }
          // smoothScrollTo可以平滑的滑动到指定位置,并打断惯性滑动
          smoothScrollTo(0, getViewTop(tab.getPosition()));
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {

      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {

      }
  };

  /**
   * ScrollView的滚动回调
   */
  public interface OnScrollCallback {
      void onScrollCallback(int l, int t, int oldl, int oldt);
  }

}

源码地址:https://github.com/ilatent/CustomView

OpenCV(开源计算机视觉库)是个强大的图像处理计算机视觉工具包,被广泛应用于各种领域,包括图像分析、机器学习人工智能。MFC(Microsoft Foundation Classes)是微软提供的套面向对象的C++库,用于构建Windows应用程序。在这个项目中,我们将OpenCV与MFC结合,利用MFC中的CRectTracker类实现用户交互式地选择图像ROI(感兴趣区域)并创建模板图像。CRectTracker类是MFC中的个实用工具,它允许用户通过拖动鼠标在窗口上画出个矩形区域,就像橡皮筋样。这个类提供了跟踪矩形边角的能力,使得用户可以方便地调整矩形的大小位置。在OpenCV中,我们需要这种交互性来让用户指定他们感兴趣的图像区域,这通常是用于匹配、识别或其他特定分析任务。在实现过程中,你需要在MFC应用中集成OpenCV库,创建个窗口控件显示图像。然后,你可以为该控件添加消息处理函数,监听WM_LBUTTONDOWN、WM_LBUTTONUPWM_MOUSEMOVE消息,这些消息分别对应于鼠标的左键按下、释放移动事件。当用户在图像上点击并拖动时,CRectTracker会跟踪这个矩形区域,并更新其位置大小。旦用户完成选择,你可以获取CRectTracker实例中的矩形坐标,并将其映射到原始图像的像素坐标。OpenCV提供了`cv::Rect`类来表示图像上的矩形区域,你可以创建个`cv::Rect`对象来保存用户选择的ROI。然后,你可以使用这个矩形区域从原始图像中裁剪出个新的图像,这就是模板图像。模板图像在计算机视觉中有多种用途,例如模板匹配。在OpenCV中,可以使用`cv::matchTemplate`函数进行模板匹配,它会计算模板图像目标图像之间的相似度,返回个匹配结果图,高亮显示出与模板最匹配的区域。为了提高用户体验,你可能还需要考虑些额外的功能,如实时预览选定的ROI,或者提供撤销/重做操作。此外,确保正确处理边界情况,比如用户选择的矩形区域超出图像边界。这个项目结合了OpenCV的强大图像处理功能MFC的用户界面优点,实现了用户友好的图像ROI选择。通过CRectTracker类,我们可以创建个交互式的图形界面,允许用户自由地选择感兴趣的图像区域,进而生成模板图像,这对于许多计算机视觉应用来说是非常有用的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值