评论

收藏

[HarmonyOS] HarmonyOS列表组件-ListContainer

移动开发 移动开发 发布于:2021-08-11 16:32 | 阅读数:440 | 评论:0

前言
我们在app开发中,列表组件绝对是使用场景最高的组件之一,鸿蒙为我们提供了ListContainer列表组件,它是一个是用来呈现连续、多行数据的组件,继承自ComponentContainer,因此它是一个容器组件,使用BaseItemProvider来存储对象。

正文
这里先简单介绍下ListContainer的基本用法:
1.在layout文件中声明ListContainer控件;
2.定义列表控件的适配器ListItemProvider;
3.在Ability中给ListContainer设置数据;
只需要三步就可以实现最基本的列表效果,这里就不贴代码了,官方文档有比较详细的说明,本文重点分析下如何通过自定义ListContainer来
实现子组件弧形排布的效果,并且随着半径和镜像距离的改变子组件的排布也不断变化,效果如下:
::: hljs-center
DSC0000.gif

:::
因为ListContainer的子组件默认是直线排列,可以通过设置LayoutManager(布局管理器)来改变子组件排列方式,但是官方只提供了TableLayoutManager(网格)和DirectionalLayoutManager(线性)两种布局管理器,很显然无法满足需求,于是设想自定义一个TurnLayoutManager继承DirectionalLayoutManager,然后重写相关方法对子组件重新排列:
然而事情并非如预想一般简单,DirectionalLayoutManager并没有对应的方法,它的父类LayoutManager也没有,惊不惊喜,意不意外?!
abstract class LayoutManager {
  public LayoutManager() {
    throw new RuntimeException("Stub!");
  }

  public void setOrientation(int orientation) {
    throw new RuntimeException("Stub!");
  }

  public int getOrientation() {
    throw new RuntimeException("Stub!");
  }
}
但是令人欣慰的是ListContainer并不是必须设置布局管理器子组件才能显示出来,于是一个大胆的念头在我的脑海中闪现:何不从ListContainer本身入手,自定义TurnListContainer类继承ListContainer,因为ListContainer继承自ComponentContainer,可以在onArrange()回调方法中修改子组件的位置以达到预期效果,事不宜迟,说干就干:
  • 1.实现ComponentContainer.ArrangeListener接口,重写onArrange()方法,在该方法中计算圆心,及x,y坐标偏移量(列表是垂直方向时计算x轴偏移量,水平方向时计算y轴偏移量)
@Override
public void onArrange() {
  //计算圆心
  this.center = deriveCenter(gravity, getOrientation(), radius, peekDistance, center);
  //设置子组件偏移  
  setChildOffsets();
}

  • 2.调用child.arrange()方法修改子组件位置(因为本文重点讲解自定义ListContainer中遇到的问题,因此圆心、子组件的坐标计算过程就不赘述了,熟悉三角函数就很容易看懂)
void setChildOffsetsVertical() {
  //遍利修改每一个子组件的位置
  for (int ii = 0; ii < getChildCount(); ii++) {
    Component child = getComponentAt(ii);
    if (child == null) {
      continue;
    }
    LayoutConfig layoutParams = child.getLayoutConfig();
    //计算x轴偏移量
    final int offsetX = (int) resolveOffsetX(radius, child.getContentPositionY() +child.getHeight() / 2.0f,
        center, peekDistance);
    final int x = gravity == Gravity.START ? offsetX + layoutParams.getMarginLeft()
        : getWidth() - offsetX - child.getWidth() - getMarginStart(layoutParams);
    //调用子组件的arrange方法修改自身位置
    child.arrange(x, child.getTop(), child.getWidth(), child.getHeight());
  }
}

  • 3.在修改半径、镜像距离、方向、文字旋转时,调用Component的postLayout()方法请求重新进行测量、布局、绘制这三个流程来更新位置,因为我的子组件是provider提供的,不牵扯测量、和绘制过程,调用postLayout()的目的只是触发onArrange回调对子组件位置修改。
/**
   * 设置半径
   *
   * @param radius 半径
   */
  public void setRadius(int radius) {
    this.radius = Math.max(radius, MIN_RADIUS);
    postLayout();
  }

  /**
   * 设置镜像距离
   *
   * @param peekDistance 镜像距离
   */
  public void setPeekDistance(int peekDistance) {
    this.peekDistance = Math.min(Math.max(peekDistance, MIN_PEEK), radius);
    postLayout();
  }

  /**
   * 设置水平方向
   *
   * @param gravity 水平方向
   */
  public void setGravity(@Gravity int gravity) {
    this.gravity = gravity;
    postLayout();
  }

  /**
   * 设置文字旋转
   *
   * @param isRotate 文字是否旋转
   */
  public void setRotate(boolean isRotate) {
    this.isRotate = isRotate;
    postLayout();
  }
准备工作告一段落,开始测试, what? 满心期待的结果并没有出现,除了设置文字旋转有效果,修改半径,镜像距离,水平方向都没效果。。。。。。这翻车来得太快就像龙卷风.
::: hljs-center
DSC0001.gif

:::
我开始陷入漫长的沉思中。。。。。。,尝试了N多种方法后依然无果,最后分析认为:我是在ListContainer的onArrange()回调中调用了子组件的onArrange()方法,有可能这两个onArrange()方法存在冲突导致子组件本身的onArrange()失效,带着些许疑问我修改了代码,设置半径、镜像距离时不用调用postLayout()来请求重新布局,直接调用child.arrange()更新子组件位置,代码修改后效果如下:
::: hljs-center
DSC0002.gif

:::
效果还可以,修改半径、镜像距离,方向都能达到预期效果,但是细心的小伙伴一定观察到了异常,。。。,静止状态下是没有问题的,一旦开始滚动就出现原始位置和修改后位置交替出现的情况,为什么呢??,因为看不到源码我也不知道listContainer滚动中的刷新逻辑,只能推测滚动事件过程中肯定是触发了重新布局的方法,导致子组件位置被反复重置。既然只有滚动时才有问题,那就从滚动事件开始入手吧,我的思路是监听滚动状态,如果已经开始滑动了,改变滚动状态跳过惯性滚动直接停止滚动:
方法1:ListContainer.ScrolledListener监听滚动,惯性滚动时设置setEnabled(false)
@Override
public void scrolledStageUpdate(Component component, int newStage) {
  switch (newStage) {
    case Component.SCROLL_IDLE_STAGE:
      //触摸滚动
      break;
    case Component.SCROLL_AUTO_STAGE:
      //惯性滚动
      break;
    case Component.SCROLL_NORMAL_STAGE:
      //停止滚动
      break;
  }
}
方法2:Component.TouchEventListener监听滚动,手指抬起时设置listContainer.setEnabled(false)
@Override
public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
  switch (touchEvent.getAction()){
    case TouchEvent.PRIMARY_POINT_DOWN:
      //按下时设置禁止滑动
      setEnabled(false);
      break;
    case TouchEvent.PRIMARY_POINT_UP:
      //抬起时设置可以滑动
      setEnabled(true);
      break;
    default:
      return true;
  }
  return false;
}
但是经过测试,两种方法都没法立即停止惯性滚动,也就是说没有办法来干预ListContainer的滚动状态,至少目前我没有找到阻止惯性滚动的相关API,那么,只能再尝试其他方法了,。。。。。。。。。。。。。。又一次我陷入漫长的沉思中。。。。。。,在尝试了各种方法都以失败告终后,最终在我锲而不舍的努力下终于得以解决,这是这个项目中我遇到的最大的坑没有之一,耗费了太多时间和精力,鸭梨好大呀,罢了罢了。。。,话不多说,直接看正解吧:
void setChildOffsetsVertical() {
  for (int ii = 0; ii < getChildCount(); ii++) {
    Component child = getComponentAt(ii);
    if (child == null) {
      continue;
    }
    LayoutConfig layoutParams = child.getLayoutConfig();
    //计算x轴偏移量
    final int offsetX = (int) resolveOffsetX(radius, child.getContentPositionY() + child.getHeight() / 2.0f,
        center, peekDistance);
    final int xx = gravity == Gravity.START ? offsetX + layoutParams.getMarginLeft()
        : getWidth() - offsetX - child.getWidth() - getMarginStart(layoutParams);
     //调用子组件的setTranslationX方法修改自身x轴偏移量
    child.setTranslationX(xx);
    //设置子组件旋转
    setChildRotationVertical(gravity, child, radius, center);
  }
对,没有错,就只是修改了一行代码,用child.setTranslationX()替换child.arrange(),就这么简单,不管你相不相信它就是这么神奇,之所以说神奇是因为看不到源码不知道ListContainer的内部滚动机制:
经过许多波折最终达到了预期的效果,肝都要爆了, 其实一开始并不觉得项目本身有多复杂,计算量也不大,直到开始做的时候问题才一一显现出来,不得不感慨,人生路上哪有那么多的顺风顺水顺心事,总会有一些波折和苦难不合时宜的出现,磕磕绊绊的人生才是完整的。。。。。。,写这个文章主要是分享下开发中我遇到的坑(主要还是想抒发下被代码虐了千百遍的爆炸心态),避免后面再有人误入歧途,浪费宝贵的时间。

结束
下面是技术总结:
  • 1.使用postLayout()请求重新布局后再调用child.arrange(),会导致child.arrange()失效;
@Override
public boolean onArrange(int i, int i1, int i2, int i3) {
  child.arrange();//此时设置子组件位置无效
  return false;
}

  • 2.child.arrange()会触发listContainer的滚动刷新机制,反复重置位置,鸿蒙调用child.arrange()修改子组件位置一切正常,但是listContainer滚动中位置会被频繁重置,如果涉及到修改子组件位置的,出现滚动中位置被反复重置的,可以尝试用child.setTranslationX(x)和child.setTranslationX(y)来代替;
  • 3.监听滚动事件
    android中有scrollVerticallyBy和scrollHorizontallyBy回调来监听横向滚动和垂直滚动,鸿蒙可以实现ListContainer.ScrolledListener接口或者Component.TouchEventListener接口监听,我这里只所以选择实现ListContainer.ScrolledListener是因为可以重写它的两个方法,onContentScrolled监听滚动中变化和scrolledStageUpdate监听滚动状态变化,会比TouchEventListener方便些;
  • 4.setEnable(false)
    这个方法可以禁止listContainer滚动,但是如果listContainer已经开始滚动了再设置setEnable(false)并不会阻止listContainer惯性滚动,禁止惯性滚动的方法目前还没有找到。

更多原创
请关注: 中软国际 HarmonyOS研发团队
在研发和使用过程中关于组件的扩展、移植,复用及数据结构和算法等问题,欢迎各位研发小伙伴交流讨论,让我们一起携手前行共建鸿蒙生态。
作者:盛禹
想了解更多关于鸿蒙的内容,请访问:
51CTO和华为官方战略合作共建的鸿蒙技术社区
https://harmonyos.51cto.com/#bkwz

关注下面的标签,发现更多相似文章