List with different column count

随声附和 提交于 2019-12-08 11:52:31

问题


As the pic show(This is drawn by PhotoShop, not implemented yet), I want to implemnt a List like this one. It has different column count, say the first row has only one item, and the others have two items . I tried to use itemRendererFunction to detect the different item(the first row treat as a rendererA, the others treat as another rendererB),but it didn't work.


回答1:


The cleanest solution to this problem, is to create a custom layout (we've discussed in the comments how Romi's solution will eventually cause too many problems). However, this is usually not an easy thing to do.

I will give you a rough sketch of what this custom layout might look like, so you can use it as a starting point to create one that does exactly what you need. To create a custom layout, you must subclass BaseLayout and override and implement its updateDisplayList and measure methods.

To make things easier (and in order not to dump 500 lines of code in here), I used some hardcoded variables for this example. It assumes there will always be two columns, the first item will always be 200x200 px, and the other items will always be 100x100 px. There is no horizontalGap or verticalGap.
The consequence is of course that you can use this custom layout (as it is now) only for this specific List and these specific ItemRenderers. If you want it to be more generic, you'll have to do a lot more calculations.

But now for the code:

public class MyCustomLayout extends LayoutBase {

    //hardcoded variables
    private var columnCount:int = 2;

    private var bigTileWidth:Number = 200;
    private var bigTileHeight:Number = 200;
    private var smallTileWidth:Number = 100;
    private var smallTileHeight:Number = 100;

    override public function updateDisplayList(width:Number, height:Number):void {
        var layoutTarget:GroupBase = target;
        if (!layoutTarget) return;

        var numElements:int = layoutTarget.numElements;
        if (!numElements) return;

        //position and size the first element
        var el:ILayoutElement = useVirtualLayout ? 
            layoutTarget.getVirtualElementAt(0) : layoutTarget.getElementAt(0);
        el.setLayoutBoundsSize(bigTileWidth, bigTileHeight);
        el.setLayoutBoundsPosition(0, 0);

        //position & size the other elements in 2 columns below the 1st element
        for (var i:int=1; i<numElements; i++) {
            var x:Number = smallTileWidth  * ((i-1) % 2);
            var y:Number = smallTileHeight * Math.floor((i-1) / 2) + bigTileHeight;

            el = useVirtualLayout ? 
                layoutTarget.getVirtualElementAt(i) : 
                layoutTarget.getElementAt(i);
            el.setLayoutBoundsSize(smallTileWidth, smallTileHeight);
            el.setLayoutBoundsPosition(x, y);
        }

        //set the content size (necessary for scrolling)
        layoutTarget.setContentSize(
            layoutTarget.measuredWidth, layoutTarget.measuredHeight
        );
    }

    override public function measure():void {
        var layoutTarget:GroupBase = target;
        if (!layoutTarget) return;

        var rowCount:int = Math.ceil((layoutTarget.numElements - 1) / 2);

        //measure the total width and height
        layoutTarget.measuredWidth = layoutTarget.measuredMinWidth = 
            Math.max(smallTileWidth * columnCount, bigTileWidth);
        layoutTarget.measuredHeight = layoutTarget.measuredMinHeight = 
            bigTileHeight + smallTileHeight * rowCount;
    }

}

And you can use it like this:

<s:List dataProvider="{dp}" height="300">
    <s:layout>
        <l:MyCustomLayout />
    </s:layout>
</s:List>



回答2:


Whenever you want to change the defined behavior of an existing component, always check first if you can solve the problem with skinning. It is a really powerful feature i Flex, and can also provide a solution in this case.

So, let's begin, assuming you already have your List, you only need to create a custom skin which "splits" the data provider in two parts, the first item, and all the others. So, let's assume we have this initial setup:

<fx:Script>
    <![CDATA[
        import mx.collections.ArrayCollection;

        [Bindable]
        private var c:ArrayCollection = new ArrayCollection([
            "String 1",
            "String 2",
            "String 3",
            "String 4",
            "String 5",
            "String 6",
            "String 7",
            "String 8",
            "String 9",
            "String 10",
            "String 11",
            "String 12",
            "String 13",
            "String 14",
            "String 15"]);
    ]]>
</fx:Script>

<s:List skinClass="CustomSkinList" dataProvider="{c}" />

As you can see, we define a custom list skin, which is just a copy of spark.skins.spark.ListSkin, the default skin for spark.components.List element.

Before we handle the data provider logic, we need to take a look at how the list items are rendered. This is done by using a DataGroup element, added to the skin, like so:

<s:Scroller left="0" top="0" right="0" bottom="0" id="scroller" minViewportInset="1" hasFocusableChildren="false">
    <!--- @copy spark.components.SkinnableDataContainer#dataGroup -->
    <s:DataGroup id="dataGroup" itemRenderer="spark.skins.spark.DefaultItemRenderer">
        <s:layout>
            <!--- The default layout is vertical and measures at least for 5 rows.  
            When switching to a different layout, HorizontalLayout for example,
            make sure to adjust the minWidth, minHeight sizes of the skin -->
            <s:VerticalLayout gap="0" horizontalAlign="contentJustify" requestedMinRowCount="5" />
        </s:layout>
    </s:DataGroup>
</s:Scroller>

Here is the place where we will have to make the changes, in order to get the first element to render differently. What we need to do, is just add another DataGroup, for rendering the first element in a custom way (this of course means using a custom item renderer). Now, our scroller looks like this:

<s:Scroller left="0"
            top="0"
            right="0"
            bottom="0"
            id="scroller"
            minViewportInset="1"
            hasFocusableChildren="false">
    <!--- @copy spark.components.SkinnableDataContainer#dataGroup -->
    <s:VGroup width="100%" height="100%">

        <s:DataGroup id="firstItemDataGroup"
                     width="100%"
                     itemRenderer="CustomItemRenderer"
                     height="20">
            <s:layout>
                <s:VerticalLayout />
            </s:layout>
        </s:DataGroup>

        <s:DataGroup id="dataGroup" itemRenderer="spark.skins.spark.DefaultItemRenderer">
            <s:layout>
                <!--- The default layout is vertical and measures at least for 5 rows.
                When switching to a different layout, HorizontalLayout for example,
                make sure to adjust the minWidth, minHeight sizes of the skin -->
                <s:TileLayout horizontalAlign="center" requestedColumnCount="2" />
            </s:layout>
        </s:DataGroup>
    </s:VGroup>
</s:Scroller>

Notice the 'firstItemDataGroup' addition, also the fact that it uses a different item renderer, than the default dataGroup element. With this new container in place we can proceed to render the elements. The custom skin need to override the parent initializationComplete() method, like so:

        override protected function initializationComplete():void
        {
            useChromeColor = true;

            if (hostComponent.dataProvider && hostComponent.dataProvider.length > 0)
            {
                var allItems:Array = hostComponent.dataProvider.toArray().concat();

                firstItemDataGroup.dataProvider = new ArrayCollection([hostComponent.dataProvider.getItemAt(0)]);

                var remainingItems:Array = allItems.concat().reverse();
                remainingItems.pop();

                var reversed:Array = remainingItems.reverse();
                dataGroupProvider = new ArrayCollection(reversed);
            }

            super.initializationComplete();
        }

What was added was just the 'if' block, and a private variable, named dataGroupProvider. This is because we will set the new dataProvider, the one starting from the second element, to the dataGroup element, in the updateDisplayList() method. Here is what it looks like:

        override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
        {
            if (getStyle("borderVisible") == true)
            {
                border.visible = true;
                background.left = background.top = background.right = background.bottom = 1;
                scroller.minViewportInset = 1;
            }
            else
            {
                border.visible = false;
                background.left = background.top = background.right = background.bottom = 0;
                scroller.minViewportInset = 0;
            }

            // Here we assign the new data provider to the dataGroup element
            if (dataGroupProvider)
                dataGroup.dataProvider = dataGroupProvider;

            borderStroke.color = getStyle("borderColor");
            borderStroke.alpha = getStyle("borderAlpha");

            super.updateDisplayList(unscaledWidth, unscaledHeight);
        }

In conclusion, just by creating a custom skin for our List element, we can use two containers for rendering the first item in a different way, from the rest of the elements. You shouldn't underestimate the power of Flex Skinning :)

Hope this helps. Have a great day!



来源:https://stackoverflow.com/questions/11897205/list-with-different-column-count

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!