HarmonyOS列表组件-ListContainer

想了解更多内容,表组请访问:

和华为官方合作共建的表组鸿蒙技术社区

https://harmonyos.51cto.com

前言

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

正文

这里先简单介绍下ListContainer的表组基本用法:

1.在layout文件中声明ListContainer控件;

2.定义列表控件的适配器ListItemProvider;

3.在Ability中给ListContainer设置数据;

只需要三步就可以实现最基本的列表效果,这里就不贴代码了,表组官方文档有比较详细的表组说明,本文重点分析下如何通过自定义ListContainer来

实现子组件弧形排布的表组效果,并且随着半径和镜像距离的表组改变子组件的排布也不断变化,效果如下:

因为ListContainer的表组子组件默认是直线排列,可以通过设置LayoutManager(布局管理器)来改变子组件排列方式,但是官方只提供了TableLayoutManager(网格)和DirectionalLayoutManager(线性)两种布局管理器,源码库很显然无法满足需求,于是设想自定义一个TurnLayoutManager继承DirectionalLayoutManager,然后重写相关方法对子组件重新排列:

然而事情并非如预想一般简单,DirectionalLayoutManager并没有对应的方法,它的父类LayoutManager也没有,惊不惊喜,意不意外?!

public 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中遇到的问题,因此圆心、子组件的香港云服务器坐标计算过程就不赘述了,熟悉三角函数就很容易看懂)

public 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? 满心期待的结果并没有出现,除了设置文字旋转有效果,修改半径,镜像距离,水平方向都没效果。。。。亿华云计算。。这翻车来得太快就像龙卷风.

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

效果还可以,修改半径、镜像距离,方向都能达到预期效果,但是细心的小伙伴一定观察到了异常,。。。,静止状态下是没有问题的,一旦开始滚动就出现原始位置和修改后位置交替出现的情况,为什么呢??,因为看不到源码我也不知道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,那么,只能再尝试其他方法了,。。。。。。。。。。。。。。又一次我陷入漫长的沉思中。。。。。。,在尝试了各种方法都以失败告终后,最终在我锲而不舍的努力下终于得以解决,这是这个项目中我遇到的最大的坑没有之一,耗费了太多时间和精力,鸭梨好大呀,罢了罢了。。。,话不多说,直接看正解吧:

public 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惯性滚动,禁止惯性滚动的方法目前还没有找到。

想了解更多内容,请访问:

和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

滇ICP备2023000592号-31