【中软国际】HarmonyOS列表组件-ListContainer 原创 精华

发布于 2021-8-5 10:07
浏览
19收藏

前言

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

正文

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

【中软国际】HarmonyOS列表组件-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? 满心期待的结果并没有出现,除了设置文字旋转有效果,修改半径,镜像距离,水平方向都没效果。。。。。。这翻车来得太快就像龙卷风.

【中软国际】HarmonyOS列表组件-ListContainer-开源基础软件社区

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

【中软国际】HarmonyOS列表组件-ListContainer-开源基础软件社区

效果还可以,修改半径、镜像距离,方向都能达到预期效果,但是细心的小伙伴一定观察到了异常,。。。,静止状态下是没有问题的,一旦开始滚动就出现原始位置和修改后位置交替出现的情况,为什么呢??,因为看不到源码我也不知道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惯性滚动,禁止惯性滚动的方法目前还没有找到。

更多原创

请关注: 中软国际 HarmonyOS研发团队
在研发和使用过程中关于组件的扩展、移植,复用及数据结构和算法等问题,欢迎各位研发小伙伴交流讨论,让我们一起携手前行共建鸿蒙生态。

作者:盛禹

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2021-8-5 10:07:34修改
22
收藏 19
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐