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