Let's work together on a simple and rough grid layout control for the main interface of Tv applications!

Posted by Blackwind on Wed, 31 Jul 2019 18:55:39 +0200

This post has been really a long time ~~, and finally I can take a breath to write a good blog, this time is really busy, iteration is too tight.Okay, let's skip the crap and get to today's topic.

Effect

Fig. 1 Tv application: the home page of DangBei Market

Figure 2 is our own simple and rough Tv application main interface grid control: TvGridLayout example

Today this article does not talk about the source code or the principle, but how to get rid of the simple and rough grid controls.

What would you do if you wanted to achieve a layout similar to the DangBei Market Home Page?At the top of the Tab bar, there is more than one screen for the card list under each Tab. Note that under the same Tab, you can cut the screen left or right; and each Tab, the card style and size under each screen are different;

Previously in Github clone others open source homepage grid layout project, found that many of them write the grid layout to death, directly in the xml write the first small card, the second card in the card..

Write-to-death is certainly not possible. With so many Tabs, there may be multiple screens under each Tab, so it is best to be able to dynamically calculate the position and size of the grid based on the layout data.

Realization

You ask me why I don't need the GridLayout implementation that comes with the system, why do I need one myself?

  • Reason 1: I forgot, I forgot the control~~

  • Reason 2: After approximating the basic use of GridLayout, we find that it is more suitable for scenarios where the card style is fixed, such as the next grid layout of a Tab, where the location and size of each card are fixed, so it is easy to implement it.

  • Reason 3: Anyway, I just want to have a ~

Okay, start the analysis, how do you want such a grid control?

Step 1: Define the layout data structure

  • ElementEntity

First, the first step is because our grid control is to support the dynamic calculation of the size and location information of each card based on the layout data. Then the layout data needs to provide the location information of each card and the horizontal and horizontal position of each screen, so the data structure of each card can be defined as follows:

public class ElementEntity implements Serializable {
    private int x;//Card coordinates
    private int y;//Card coordinates
    private int row;//Card length
    private int column;//Card width

    private String imgUrl;
}

Because the grid control of our network is to dynamically calculate the size and location of the card, there are many ways to calculate it. We take the method of dividing the current screen into n cells according to the layout data, and using the upper left corner of each cell as the starting point of coordinates, then each card needs to provide the starting point of x,y coordinates.In calculating its position, row, column indicates that the current card occupies row cells horizontally and column cells vertically.

As long as each card provides this data, you can implement different card style and size according to the different data of each card.

  • ScreenEntity

Then the card bits belong to one of the screens under each Tab, so all the card bits on each screen form a set of card bit lists. The different screen card bit lists should be independent, so the data structure of each screen can be defined as follows:

public class ScreenEntity implements Serializable {
    private int row;//Divide horizontally into rows
    private int column;//Divide the vertical direction into columns
    //row, column is used to divide the current screen evenly into row * column cells

    private List<ElementEntity> elementList;
}

Even though the style of each screen under the same Tab is different, each screen should be divided into several small squares on average, depending on the screen itself.

  • MenuEntity

Each Tab can represent a menu, and there are multiple screen cards under the Tab, so its data structure can be defined as follows:

public class MenuEntity implements Serializable {
    private List<ScreenEntity> screenList;//There may be multiple screens under a Tab
}

  • LayoutEntity

Home page may contain more than one Tab, so the layout data of the home page can be defined as follows:

public class LayoutEntity {
    private List<MenuEntity> menuList;//May contain more than one Tab menu
}
  • json

To summarize, the layout data structure of the home page can look like this:

{
    "menuList": [
        {
            "menuName": "Film and TV Entertainment",
            "screenList": [
                {
                    "row": 6,
                    "column": 4,
                    "elementList": [
                        {
                            "x": 3,
                            "y": 1,
                            "row": 3,
                            "column": 1
                        },
                        {
                            "x": 4,
                            "y": 1,
                            "row": 6,
                            "column": 1
                        },
                        {
                            "x": 2,
                            "y": 4,
                            "row": 3,
                            "column": 2
                        },
                        {
                            "x": 1,
                            "y": 1,
                            "row": 6,
                            "column": 1
                        },
                        {
                            "x": 2,
                            "y": 1,
                            "row": 3,
                            "column": 1
                        }
                    ]
                }
            ]
        }
    ]
}

This first step is critical, especially the data structure of each card and the data structure definition of each screen, since the dynamic implementation of the grid layout is based on this data.

Step 2: Customize TvGridLayout

Think about the grid control we want to support dynamic calculation of the size and location of the card, and support the card beyond the screen to draw outside the screen, so that when the screen is cut, you can slide directly to the next screen display.

Based on these two points, we don't inherit from ViewGroup and write it all by ourselves. Simple rough points, we inherit from FrameLayout, then simply specify the location in absolute coordinate system by LayoutParams of FrameLayout, and add it in F with View of card styleRameLayout is fine.

Okay, start work:

public class TvGridLayout extends FrameLayout {
    ...
    private Adapter mAdapter;

    public TvGridLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public void setAdapter(Adapter adapter) {
        mAdapter = adapter;
        ...

            layoutChildren();//Dynamic calculation of each card size, location for layout
    }

    //Card Information Source
    public static abstract class Adapter { 
        ...
    }
}

Think about how easy it is to use a grid control.

Referring to the idea of RecyclerView, the TvGridLayout grid control only provides pure layout functionality. As for how long each card is, its size, location, and so on, are left to the Adapter to implement.

That is, to use the TvGridLayout grid control, we simply write an adapter inherited from TvGridLayout.Adapter as RecyclerView does and implement its abstract method to provide TvGridLayout with the necessary layout data.

Step 3: Customize the Adapter

So what layout data does TvGridLayout need, in other words, how do we define the abstract method of the Adapter?

Consider that our grid controls support multiple screens, and each screen can have more than one card, so we need the total number of screens and the number of cards below each screen:

  • public abstract int getPageCount()
  • public abstract int getChildCount(int pageIndex)

And the style of each screen can be different. In other words, how many small cells, rows and columns, each screen should be divided equally. This data is also needed, so:

  • public abstract int getPageRow(int pageIndex)​
  • public abstract int getPageColumn(int pageIndex)

The overall style has been finalized. Next comes each card. What information does the card need?Actually, there are three points, location, size and length.For convenience, we can encapsulate location and size information after a layer of conversion, then:

  • public abstract ItemCoordinate getChildCoordinate(int pageIndex, int childIndex)
  • ​public abstract View getChildView(int groupPosition, int childPosition, int childW, int childH);

Okay, so that you have all the layout data you need for TvGridLayout. As long as you inherit TvGridLayout.Adapter and implement the corresponding abstraction method, and provide the corresponding layout data according to the data structure defined in our first step, the layout work is left to TvGridLayout internally.That's it.

Take a look at the whole code:

public static abstract class Adapter {
    public abstract int getPageRow(int pageIndex);
    public abstract int getPageColumn(int pageIndex);
    public abstract ItemCoordinate getChildCoordinate(int pageIndex, int childIndex);
    public abstract View getChildView(int groupPosition, int childPosition, int childW, int childH);
    public abstract int getChildCount(int pageIndex);
    public abstract int getPageCount();
    protected void onSwitchAdapter(Adapter newAdapter, Adapter oldAdapter) {}
}

The way you use it is very similar to RecyclerView, which is simple and rough.One difference is that in RecyclerView.Adapter, the size of our item view is up to you, as big as you want.Here, however, the size and location of the item View are determined by the layout data sent down by the service side, which is handled directly by the TvGridLayout itself, so you can see that in the parameters of the getChildView() method, we passed the current size of the card to the Adapter, which may be a little different from what we normally use.Dissimilarity.

Step 4: Dynamic Layout

Now that the data structure of the layout data is set and TvGridLayout gets the layout data it needs through the Adapter, the next step is to make dynamic calculations based on the data and complete the layout work.These jobs are done inside TvGridLayout, and the time to trigger the layout work can be in setAdapter(), and when an Adapter comes in from outside, we can do the layout work by calling it layoutChildren().

private void layoutChildren() {
    //Convenient optimization
    layoutChildrenOfPages(0, mAdapter.getPageCount());
}

private void layoutChildrenOfPages(int fromPage, int toPage) {
    //1\. Get the width and height of the grid control (i.e., the size of each screen)
    int contentWidth = mWidth - getPaddingLeft() - getPaddingRight();
    int contentHeight = mHeight - getPaddingTop() - getPaddingBottom();
    //2\. Traverse each screen
    for (int j = fromPage; j < toPage; j++) {
        //3\. Get the number of rows and columns on screen j
        int column = mAdapter.getPageColumn(j);//Number of columns
        int row = mAdapter.getPageRow(j);//Number of rows
        //4\. Average the current j-screen into column * row cells based on the number of rows and columns and the size of the grid control
        float itemWidth = (contentWidth) * 1.0f / column;//Width of each cell
        float itemHeight = (contentHeight) * 1.0f / row;//Height of each cell

        int pageWidth = 0;//The width of each screen is not necessarily full of grid controls. It is possible that the current screen width is only half, so you need to record what the width of the current screen is.

         //5\. Traverse through each card in the current j screen
        for (int i = 0; i < mAdapter.getChildCount(j); i++) {
            //6\. Get the location and size information of the current card
            ItemCoordinate childCoordinate = mAdapter.getChildCoordinate(j, i);
            if (childCoordinate == null) {
                //7\. If the current card does not have location size information
                continue;
            }
            int pointStartX = childCoordinate.start.x;
            int pointStartY = childCoordinate.start.y;
            int pointEndX = childCoordinate.end.x;
            int pointEndY = childCoordinate.end.y;

            //8\. Calculate the size of the card based on its layout information (position, length)
            int width = (int) ((pointEndX - pointStartX) * itemWidth);
            int height = (int) ((pointEndY - pointStartY) * itemHeight);

            //9\. Calculates the location of a card based on its layout information (position, length), directly calculating the absolute position in the parent control coordinate system
            int marginLeft = (int) (pointStartX * itemWidth + contentWidth * j);
            int marginTop = (int) (pointStartY * itemHeight);

            if (marginLeft < 0) {
                marginLeft = 0;
            }
            if (marginTop < 0) {
                marginTop = 0;
            }

            //10\. Get the style of the card, what do you want, Adapter decides
            View view = mAdapter.getChildView(j, i, width, height);
            if (view == null) {
                //11\. If the current location of the card is not configured, it will not participate in the layout
                continue;
            }

            //12\. LayoutParams is used to lay out the layout, and parameters are passed in to the card size.
            LayoutParams params = new LayoutParams(width - mItemSpace * 2, height - mItemSpace * 2);//Deduction Spacing

            //13\. Use leftMargin,topMargin to determine the location of the card
            params.topMargin = marginTop + mItemSpace;
            params.leftMargin = marginLeft + mItemSpace;
            //14\. Store the card information directly in the LayoutParams of the card for easy subsequent direct use
            params.itemCoordiante = childCoordinate;
            params.pageIndex = j;

            //15\. Record the length of the current screen, because each screen may not be filled with the entire parent control. There may be three screens under a Tab, but the second screen is only configured with half of the card bits
            int maxWidth = marginLeft + width - contentWidth * j;
            pageWidth = Math.max(pageWidth, maxWidth);

            //16\. Record the total length of the grid control under this Tab
            int maxRight = marginLeft + width;
            mRightEdge = Math.max(mRightEdge, maxRight);

            //17\. Record the first card position on each screen for easy follow-up if you need to manipulate the default focus
            if (childCoordinate.start.x == 0 && childCoordinate.start.y == 0) {
                mFirstChildOfPage.put(j, view);
            }

            //18\. Add to parent container to complete layout
            if (j == 0 && childCoordinate.start.x == 0 && childCoordinate.start.y == 0) {
                addView(view, 0, params);
            } else {
                addView(view, params);
            }   
        }
    }
}

Layout logic for dynamic computing Look at the code notes, which are detailed ~

In addition, we encapsulate the location and size information of the card in ItemCoordinate for convenience:

static class ItemCoordinate {
    public Point start;//Upper left coordinate
    public Point end;//Lower right corner coordinates
}

As long as there are upper left and lower coordinates, the location and size of the card can be determined.In addition, the coordinate system here is not an Android coordinate system, it is a coordinate system with each cell as the unit, not a specific px value, it is easy to understand if you draw a picture:

Also, we customized a LayoutParams that inherits from FrameLayout.LayoutParams. Nothing special, just to bind and store information about some of the cards directly to the cards for later use, without having to create a map to maintain and manage it yourself:

private static class LayoutParams extends FrameLayout.LayoutParams {
    ItemCoordinate mItemCoordinate;//Position and size information of card positions
    int pageIndex;//Which screen does the card belong to

    ...
}

Step 5: Preliminary use

Well, here, a simple and rough grid control is implemented, which supports dynamic calculation of the location and size of the card according to the layout data; supports multiple screens under a Tab, and the size and style of each screen can be determined by itself;

Just think, it's easy to define the data structure of the layout data, then the server needs to provide the location and size information of each screen and each card bit. Finally, like RecyclerView, you can write an Adapter to provide the corresponding data and the View of the card bit.

But at this point, the controls don't support sliding.

Because the TvGridLayout we've written here doesn't handle sliding, of course it can't. To make it slide, it's also very simple. Modify the xml layout file and put a HorizontalScrollView control on the outside of TvGridLayout, so it can slide.

However, there are some drawbacks to this kind of sliding. The sliding strategy can only follow the system, and the length of sliding cannot be modified.In this case, you may not be able to satisfy the tough taste of the product.Now that the grid controls have been built by themselves, the simple sliding has been achieved by itself. Do you want to slide as far as you want, as long as you want, and in the fear of serving a bad product?

However, this article has been a long one. Let's talk about how to slide by yourself in the next one.

Summary

Finally, let's summarize the grid control we created ourselves:

  • Advantages: Simple, rough, supports multiple screens, supports dynamic setting of different screen styles and sizes, supports dynamic setting of card position and size
  • Advantages: Wait until the next section is finished with your own sliding function, then support how to slide, not afraid to wait for the product
  • Advantages: Support for each screen card bit does not necessarily have to be fully screen-filled, screen size does not necessarily have to be full of parent controls
  • Disadvantages: Immature, unstable, there may be some problems
  • Disadvantages: Without such considerations as reuse, all the cards on all screens are drawn after setAdapter() is set
  • Disadvantages: Layout data needs to be provided by the server

Anyway, first come out with a simple and rough control, and then optimize it step by step.

Give a compliment if you think the article is well written?If you think it's worth improving, please leave a message for me.We will inquire carefully and correct the deficiency.Thank you.

I hope you can forward share and care about me, and update the technology in the future. Thank you for your support!

Forward+Praise+Attention, Get the latest knowledge at the first time

Android architects have a long way to go. Let's work together!

The following cracks are recommended!!!

Finally, I wish you all a happy life ~

Topics: Android xml github network