Proper way to implement custom View and dynamically added children

759 views Asked by At

I'm extending a HorizontalScrollView. This layout will add children to a LinearLayout when the public setItems method is called. These children views are dynamically inflated and their widths depend on the parent (fill parent when only one view, and 1/2 parent when >= 2 items).

<!-- custom_layout.xml -->
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/container"
        android:orientation="horizontal"
        android:layout_width="wrap_content"
        android:layout_height="match_parent">
    </LinearLayout>
</merge>

Sometimes, setItems is called in a callback of a network request, so the layout might already be fully inflated. Where should I be calling updateView which needs the parent's width and to add inflated children? I put the call in both onSizeChanged and setItems like below.

public class CustomLayout extends HorizontalScrollView {

    private LinearLayout container; 
    private List<Item> items;

    public void setItems(List<Item> items) {
        this.items = items;
        updateView();
    }

    public CustomLayout(Context context) {
        super(context);
        init();
    }

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

    private void init() {
        View v = inflate(getContext(), R.layout.custom_layout, this);
        container = (LinearLayout) v.findViewById(R.id.container);
        updateView();
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        updateView();
    }

    public void updateView() {
        if (items == null) {
            return;
        }

        container.removeAllViews();

        int width = getWidth();
        if (items.size() > 1) {
            width = width * 1 / 2;
        }

        for (final Item item : items) {        
            View child = LayoutInflator.from(getContext(), R.layout.custom_item, container, false);                   
            child.setLayoutParams(new LinearLayout.LayoutParams(width, LinearLayout.LayoutParams.MATCH_PARENT));
            container.addView(child);
        }
    }

}

I don't know why this way does not always work. Sometimes, the child, container just fails to render for some reason. Seeing the layout hierarchy shows that HorizontalScrollView has no children. What happened to container?

Also, it seems that even when placed in a network call, setItems is called before onSizeChanged, which makes me think that onSizeChanged is the wrong place to do updateView?

1

There are 1 answers

0
romtsn On

Try to place the updateView() method inside onMeasure instead of onSizeChanged, and use getMeasuredWidth() instead of getWidth():

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    updateView();
}

public void updateView() {
    if (items == null) {
        return;
    }

    container.removeAllViews();

    int width = getMeasuredWidth();
    if (items.size() > 1) {
        width = width * 1 / 2;
    }

    for (final Item item : items) {        
        View child = LayoutInflator.from(getContext(), R.layout.custom_item, container, false);                   
        child.setLayoutParams(new LinearLayout.LayoutParams(width, LinearLayout.LayoutParams.MATCH_PARENT));
        container.addView(child);
    }
}

Also, if you call setItems() method from callback of async request, dont forget to run it on UI thread by using runOnUiThread method of Activity.