在鸿蒙中实现类似瀑布流效果

华为开发者论坛      2022-02-11     798

关键词:

简介
  鸿蒙OS 开发SDK中对于长列表的实现ListContainer的实现较为简单,没法想RecyclerView一样通过使用不同的LayoutManager来实现复杂布局因此没法快速实现瀑布流效果。
  但鸿蒙OS也都支持控件的Measure(onEstimateSize),layout(onArrange) 和事件的处理。完全可以在鸿蒙OS中自定义一个布局来实现RecyclerView+LayoutManager的效果,以此来实现瀑布流等复杂效果。

自定义布局

  对于鸿蒙OS自定义布局在官网上有介绍,主要实现onEstimateSize来测量控件大小和onArrange实现布局,这里我们将子控件的确定和测量摆放完全交LayoutManager来实现。同时我们要支持滑动,这里用Component.DraggedListener实现。因此我们的布局容器十分简单,调用LayoutManager进行测量布局,同时对于滑动事件,确定滑动后的视窗,调用LayoutManager的fill函数确定填满视窗的子容器集合,然后触发重新绘制。核心代码如下
  

public class SpanLayout extends ComponentContainer implements ComponentContainer.EstimateSizeListener,
        ComponentContainer.ArrangeListener, Component.CanAcceptScrollListener, Component.ScrolledListener, Component.TouchEventListener, Component.DraggedListener {

   
    private BaseItemProvider mProvider;
    public SpanLayout(Context context) {
        super(context);
        setEstimateSizeListener(this);
        setArrangeListener(this);
        setDraggedListener(DRAG_VERTICAL,this);
        
    }



    @Override
    public boolean onEstimateSize(int widthEstimatedConfig, int heightEstimatedConfig) {
        int width = Component.EstimateSpec.getSize(widthEstimatedConfig);
        int height = Component.EstimateSpec.getSize(heightEstimatedConfig);
        setEstimatedSize(
                Component.EstimateSpec.getChildSizeWithMode(width, widthEstimatedConfig, EstimateSpec.UNCONSTRAINT),
                Component.EstimateSpec.getChildSizeWithMode(height, heightEstimatedConfig, EstimateSpec.UNCONSTRAINT));
        mLayoutManager.setEstimateSize(widthEstimatedConfig,heightEstimatedConfig);
//        measureChild(widthEstimatedConfig,heightEstimatedConfig);
        return true;
    }


    @Override
    public boolean onArrange(int left, int top, int width, int height) {


        //第一次fill,从item0开始一直到leftHeight和rightHeight都大于height为止。
        if(mRecycler.getAttachedScrap().isEmpty()){
           mLayoutManager.fill(left,top,left+width,top+height,DIRECTION_UP);
        }
//        removeAllComponents(); //调用removeAllComponents的话会一直出发重新绘制。
        for(RecyclerItem item:mRecycler.getAttachedScrap()){
            item.child.arrange(item.positionX+item.marginLeft,scrollY+item.positionY+item.marginTop,item.width,item.height);
        }
        return true;
    }


    @Override
    public void onDragStart(Component component, DragInfo dragInfo) {
        startY = dragInfo.startPoint.getPointYToInt();
    }

    @Override
    public void onDragUpdate(Component component, DragInfo dragInfo) {
        int dt = dragInfo.updatePoint.getPointYToInt() - startY;
        int tryScrollY = dt + scrollY;
        startY = dragInfo.updatePoint.getPointYToInt();
        mDirection = dt<0?DIRECTION_UP:DIRECTION_DOWN;
        mChange = mLayoutManager.fill(0, -tryScrollY,getEstimatedWidth(),-tryScrollY+getEstimatedHeight(),mDirection);
        if(mChange){
            scrollY = tryScrollY;
            postLayout();
        }

    }
}

瀑布流LayoutManager
LayoutManager主要是用来确定子控件的布局,重点是要实现fill函数,用于确认对于一个视窗内的子控件。

我们定义一个Span类,来记录某一列瀑布当前startLine和endLine情况,对于spanNum列的瀑布流,我们创建Span数组来记录情况。

例如向上滚动,当一个子控件满足bottom小于视窗top时需要回收,当一个子控件的bottom小于视窗的bottom是说明其下方需有子控件填充。由于瀑布流是多列的且每个子控件高度不同,因此我们不能简单的判断当前显示的第一个子控件是否要回收,最后一个子控件下方是否需要填充来完成充满视窗的工作。我们用while循环+双端队列,通过保证所有的Span其startLine都小于视窗top,endLine都大于视窗bottom来完成充满视窗的工作。核心fill函数实现如下:

public synchronized boolean fill(float left,float top,float right,float bottom,int direction){

    int spanWidth = mWidthSize/mSpanNum;
    if(mSpans == null){
        mSpans = new Span[mSpanNum];
        for(int i=0;i<mSpanNum;i++){
            Span span = new Span();
            span.index = i;
            mSpans[i] = span;
            span.left = (int) (left + i*spanWidth);
        }
    }

    LinkedList<RecyclerItem> attached = mRecycler.getAttachedScrap();
    if(attached.isEmpty()){
        mRecycler.getAllScrap().clear();
        int count = mProvider.getCount();
        int okSpan = 0;
        for (int i=0;i<count;i++){
            Span span = getMinSpanWithEndLine();
            RecyclerItem item = fillChild(span.left,span.endLine,i);
            item.span = span;
            if(item.positionY>=top && item.positionY<=bottom+item.height){//在显示区域
                mRecycler.addItem(i,item);
                mRecycler.attachItemToEnd(item);
            }else{
                mRecycler.recycle(item);
            }


            span.endLine += item.height+item.marginTop+item.marginBottom;
            if(span.endLine>bottom){
                okSpan++;
            }
            if(okSpan>=mSpanNum){
                break;
            }
        }
        return true;
    }else{
        if(direction == DIRECTION_UP){
            RecyclerItem last = attached.peekLast();
            int count = mProvider.getCount();
            if(last.index == count-1 && last.getBottom()<=bottom){//已经到底
                return false;
            }else{
                //先回收
                RecyclerItem first = attached.peekFirst();
                while(first != null && first.getBottom()<top){
                    mRecycler.recycle(first);//recycle本身会remove
                    first.span.startLine += first.getVSpace();
                    first = attached.peekFirst();
                }

                Span minEndLineSpan = getMinSpanWithEndLine();
                int index = last.index+1;
                while(index<count && minEndLineSpan.endLine<=bottom){//需要填充
                    RecyclerItem item;
                    if(mRecycler.getAllScrap().size()>index){
                        item = mRecycler.getAllScrap().get(index);
                        mRecycler.recoverToEnd(item);
                    }else{
                        item = fillChild(minEndLineSpan.left,minEndLineSpan.endLine,index);
                        item.span = minEndLineSpan;
                        mRecycler.attachItemToEnd(item);
                        mRecycler.addItem(index,item);
                    }
                    item.span.endLine += item.getVSpace();
                    minEndLineSpan = getMinSpanWithEndLine();
                    index++;
                }
                return true;
            }
        }else if(direction == DIRECTION_DOWN){
            RecyclerItem first = attached.peekFirst();
            int count = mProvider.getCount();
            if(first.index == 0 && first.getTop()>=top){//已经到顶
                return false;
            }else{
                //先回收
                RecyclerItem last = attached.peekLast();
                while(last != null && last.getTop()>bottom){
                    mRecycler.recycle(last);//recycle本身会remove
                    last.span.endLine -= last.getVSpace();
                    last = attached.peekFirst();
                }

                Span maxStartLineSpan = getMaxSpanWithStartLine();
                int index = first.index-1;
                while(index>=0 && maxStartLineSpan.startLine>=top){//需要填充
                    RecyclerItem item = mRecycler.getAllScrap().get(index);
                    if(item != null){
                        mRecycler.recoverToStart(item);
                        item.span.startLine -= item.getVSpace();
                    }else{
                        //理论上不存在
                    }
                    maxStartLineSpan = getMaxSpanWithStartLine();
                    index--;
                }

                return true;
            }
        }
    }

    return true;

}

Item回收
对于长列表,肯定要有类似于RecyclerView的回收机制。item的回收和复原在LayoutManager的fill函数中触发,通过Reycler实现。

简单的使用了mAttacthedScrap来保存当前视窗上显示的Item和mCacheScrap来保存被回收的控件。这里的设计就是对RecyclerView的回收机制的简化。

不同的是参考Flutter中三棵树的概念,定义了RecycleItem类,用来记录每个Item的左上角坐标和宽高值,只有在视窗上显示的Item会绑定组件。由于未绑定组件时的RecycleItem是十分轻量级的,因此内存的损耗基本可以忽略。我们用mAllScrap来按顺序保存所有的RecycleItem对象,用来复用。当恢复一个mAllScrap中存在的Item时,其坐标和宽高都已经确定。

Recycler的实现核心代码如下:

public class Recycler {

    public static final int DIRECTION_UP = 0;
    public static final int DIRECTION_DOWN = 2;

    private ArrayList<RecyclerItem> mAllScrap = new ArrayList<>();
    private LinkedList<RecyclerItem> mAttachedScrap = new LinkedList<>();
    private LinkedList<Component> mCacheScrap = new LinkedList<Component>();
    private BaseItemProvider mProvider;
    private SpanLayout mSpanLayout;
    private int direction = 0;

    public Recycler(SpanLayout layout, BaseItemProvider provider) {
        this.mSpanLayout = layout;
        this.mProvider = provider;
    }

    public ArrayList<RecyclerItem> getAllScrap() {
        return mAllScrap;
    }

    public LinkedList<RecyclerItem> getAttachedScrap() {
        return mAttachedScrap;
    }

    public void cacheItem(int index, RecyclerItem item) {
        mAllScrap.add(index, item);
    }

    public void attachComponent(RecyclerItem item) {
        mAttachedScrap.add(item);
    }

    public Component getView(int index, ComponentContainer container) {
        Component cache = mCacheScrap.poll();
        return mProvider.getComponent(index, cache, container);
    }

    public void addItem(int index,RecyclerItem item) {
        mAllScrap.add(index,item);
    }

    public void attachItemToEnd(RecyclerItem item) {
        mAttachedScrap.add(item);
    }

    public void attachItemToStart(RecyclerItem item) {
        mAttachedScrap.add(0,item);
    }

    public void recycle(RecyclerItem item) {
        mSpanLayout.removeComponent(item.child);
        mAttachedScrap.remove(item);
        mCacheScrap.push(item.child);
        item.child = null;
    }

    public void recoverToEnd(RecyclerItem item) {
        Component child = mProvider.getComponent(item.index, mCacheScrap.poll(), mSpanLayout);
        child.estimateSize(
                Component.EstimateSpec.getSizeWithMode(item.width, Component.EstimateSpec.PRECISE),
                Component.EstimateSpec.getSizeWithMode(item.height, Component.EstimateSpec.PRECISE)
        );
        item.child = child;
        mAttachedScrap.add(item);
        mSpanLayout.addComponent(child);
    }

    public void recoverToStart(RecyclerItem item) {
        Component child = mProvider.getComponent(item.index, mCacheScrap.poll(), mSpanLayout);
        child.estimateSize(
                Component.EstimateSpec.getSizeWithMode(item.width, Component.EstimateSpec.PRECISE),
                Component.EstimateSpec.getSizeWithMode(item.height, Component.EstimateSpec.PRECISE)
        );
        item.child = child;
        mAttachedScrap.add(0,item);
        mSpanLayout.addComponent(child);
    }


}

总结
鸿蒙OS的开发SDK中基础能力都已经提供全面了,完全可以用来实现一些复杂效果。这里实现的SpanLayout+LayoutManager+Recycler的基本是一个完整的复杂列表实现,其他布局效果也可以通过实现不同的LayoutManager来实现。

完整代码在本人的码云项目上 ,在com.profound.notes.component包下,路过的请帮忙点个star。https://gitee.com/profound-la...

原文链接:https://developer.huawei.com/...
原作者:zjwujlei
  

怎样在网页中做出瀑布流效果?

  在网页中实现瀑布流效果方法:  1.传统多列浮动  各列固定宽度,并且左浮动;  一列中的数据块为一组,列中的每个数据块依次排列即可;  更多数据加载时,需要分别插入到不同的列上。  2.CSS3定义  由chrom... 查看详情

前端用jquery实现瀑布流效果

jQuery实现瀑布流效果何为瀑布流:  瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布... 查看详情

如何在hexo中实现自适应响应式相册功能

用最清晰简洁的方法整合一个响应式相册效果技术选型由于我选用的主题使用了fancyBox作为图片弹出展示的框架,查看后表示很不错,能满足需要http://fancyapps.com/fancybox/3/图片加载可能会太慢,所以还需要一个图片延迟加载插件La... 查看详情

jquery实现无限滚动瀑布流实现原理

现在类似于pinterest这类的表现效果很火,其实我比较中意的是他的布局效果,而不是那种瀑布流。虽然我不是特别喜欢这种瀑布流的表现样式,但是还是写了几篇关于无限滚动瀑布流效果的文章,Infinitescroll+Masonry=无限滚动瀑布... 查看详情

js实现瀑布流以及加载效果

一、瀑布流是个啥?  瀑布流,是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。  最早采用瀑布流布局的网站是Pinterest,逐渐... 查看详情

flutter自适应瀑布流(代码片段)

...f1a;在电商app经常会看到首页商品推荐的瀑布流,或者类似短视频app首页也是瀑布流,这些都是需要自适应的,才能给用户带来好的体验(具体代码请联系我,当天会回复)话不多说先上效果图:根据效果图... 查看详情

利用js实现简单的瀑布流效果

一.瀑布流之准备工作    首先声明下,为了方便演示和联系,我使用的是本地图片,如果大家有需要的话可以尝试着写下网络的,不过本地和远端的大致是相同的.那么我就来简单介绍下本地的瀑布流效果吧,我们要先准备好... 查看详情

js实现瀑布流加载图片效果

   今天学习了一个瀑布流加载效果,很多网站都有瀑布流效果,瀑布流就是很多产品显示在网页上,宽相同,高度不同,表现为多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。... 查看详情

如何在 Chromecast 中实现 Ustream HLS 流?

】如何在Chromecast中实现UstreamHLS流?【英文标题】:HowcanIimplementanUstreamHLSstreaminChromecast?【发布时间】:2015-03-0619:50:55【问题描述】:我一直在尝试在Chromecast中实现直播。使用Ustreamapi,我可以获得HLS流链接。在GoogleCast文档中,... 查看详情

如何在android中实现材料设计圆形进度条

】如何在android中实现材料设计圆形进度条【英文标题】:Howtoimplementamaterialdesigncircularprogressbarinandroid【发布时间】:2015-01-0501:04:38【问题描述】:我想制作一个类似于InboxbyGmailandroid应用中的MaterialDesign圆形进度条。我如何实现... 查看详情

如何在张量流中实现提前停止

】如何在张量流中实现提前停止【英文标题】:howtoimplementearlystoppingintensorflow【发布时间】:2018-03-0719:18:08【问题描述】:deftrain():#Modelmodel=Model()#Loss,Optimizerglobal_step=tf.Variable(1,dtype=tf.int32,trainable=False,name=\'global_step\')loss_ 查看详情

如何在社交网络中实现活动流

】如何在社交网络中实现活动流【英文标题】:Howtoimplementtheactivitystreaminasocialnetwork【发布时间】:2010-11-2911:03:24【问题描述】:我正在开发自己的社交网络,但我在网络上没有找到用户操作流的实现示例...例如,如何过滤每个... 查看详情

状态改变时在 React JS 中实现过渡效果

】状态改变时在ReactJS中实现过渡效果【英文标题】:ImplementingtransitioneffectsinReactJSwhenstatechanges【发布时间】:2019-03-0114:31:09【问题描述】:我在React页面上有一张图片。当状态更新为新图像时,我想执行以下过渡效果:原始图像... 查看详情

关于jquery中实现放大镜效果

1.1.1摘要相信大家都见过或使用过放大镜效果,甚至实现过该效果,它一般应用于放大查看商品图片,一些电商网站(例如:凡客,京东商城,阿里巴巴等)都有类似的图片查看效果。在接下来的博文中,我们将向大家介绍通过j... 查看详情

uwp中实现大爆炸效果(代码片段)

原文:UWP中实现大爆炸效果(一)自从老罗搞出大爆炸之后,各家安卓都内置了类似功能。UWP怎么能落下呢,在这里我们就一起撸一个简单的大爆炸实现。闲话不说,先上效果:因为代码太多,所以我打算写成一个系列,下面是... 查看详情

如何在浏览器中实现 HTTP/2 流连接?

】如何在浏览器中实现HTTP/2流连接?【英文标题】:HowtoimplementHTTP/2streamconnectioninbrowser?【发布时间】:2019-02-1519:06:35【问题描述】:如今,HTTP/2的性能正在上升。Node.js的最新版本很好地支持了HTTP/2。https://nodejs.org/api/http2.html但... 查看详情

wpf下制作的简单瀑布流效果(代码片段)

原文:WPF下制作的简单瀑布流效果最近又在搞点小东西,美化界面的时候发现瀑布流效果比较不错.顺便就搬到了WPF,下面是界面 我对WEB前端不熟,JS和CSS怎么实现的,我没去研究过,这里就说下WPF的实现思路,相当简单.1.最重要的就... 查看详情

如何在颤动中实现这种效果/布局?

】如何在颤动中实现这种效果/布局?【英文标题】:Howtoachievethiseffect/layoutinflutter?【发布时间】:2021-10-0520:29:18【问题描述】:我发现,这种搜索效果是内置在ios框架中的。但是这种布局如何在Flutter中实现呢?【问题讨论】:... 查看详情