16

I keep running into a sizing and layout problem for custom views and I'm wondering if anyone can suggest a "best practices" approach. The problem is as follows. Imagine a custom view where the height required for the content depends on the width of the view (similar to a multi-line TextView). (Obviously, this only applies if the height isn't fixed by the layout parameters.) The catch is that for a given width, it's rather expensive to compute the content height in these custom views. In particular, it's too expensive to be computed on the UI thread, so at some point a worker thread needs to be fired up to compute the layout and when it is finished, the UI needs to be updated.

The question is, how should this be designed? I've thought of several strategies. They all assume that whenever the height is calculated, the corresponding width is recorded.

The first strategy is shown in this code:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = measureWidth(widthMeasureSpec);
    setMeasuredDimension(width, measureHeight(heightMeasureSpec, width));
}

private int measureWidth(int widthMeasureSpec) {
    // irrelevant to this problem
}

private int measureHeight(int heightMeasureSpec, int width) {
    int result;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        if (width != mLastWidth) {
            interruptAnyExistingLayoutThread();
            mLastWidth = width;
            mLayoutHeight = DEFAULT_HEIGHT;
            startNewLayoutThread();
        }
        result = mLayoutHeight;
        if (specMode == MeasureSpec.AT_MOST && result > specSize) {
            result = specSize;
        }
    }
    return result;
}

When the layout thread finishes, it posts a Runnable to the UI thread to set mLayoutHeight to the calculated height and then call requestLayout() (and invalidate()).

A second strategy is to have onMeasure always use the then-current value for mLayoutHeight (without firing up a layout thread). Testing for changes in width and firing up a layout thread would be done by overriding onSizeChanged.

A third strategy is to be lazy and wait to fire up the layout thread (if necessary) in onDraw.

I would like to minimize the number of times a layout thread is launched and/or killed, while also calculating the required height as soon as possible. It would probably be good to minimize the number of calls to requestLayout() as well.

From the docs, it's clear that onMeasure might be called several times during the course of a single layout. It's less clear (but seems likely) that onSizeChanged might also be called several times. So I'm thinking that putting the logic in onDraw might be the better strategy. But that seems contrary to the spirit of custom view sizing, so I have an admittedly irrational bias against it.

Other people must have faced this same problem. Are there approaches I've missed? Is there a best approach?

Ted Hopp
  • 232,168
  • 48
  • 399
  • 521
  • Wouldnt it be possible to use a OnGlobalLayoutListener() on the view whose height is required ? – lokoko Feb 07 '13 at 13:25
  • @lokoko - I'm not sure I understand the suggestion. Can you elaborate? The issue is: when should I initiate the expensive layout height calculations? – Ted Hopp Feb 07 '13 at 16:27

5 Answers5

10

I think the layout system in Android wasn't really designed to solve a problem like this, which would probably suggest changing the problem.

That said, I think the central issue here is that your view isn't actually responsible for calculating its own height. It is always the parent of a view that calculates the dimensions of its children. They can voice their "opinion", but in the end, just like in real life, they don't really have any real say in the matter.

That would suggest taking a look at the parent of the view, or rather, the first parent whose dimensions are independent of the dimensions of its children. That parent could refuse layouting (and thus drawing) its children until all children have finished their measuring phase (which happens in a separate thread). As soon as they have, the parent requests a new layout phase and layouts its children without having to measure them again.

It is important that the measurements of the children don't affect the measurement of said parent, so that it can "absorb" the second layout phase without having to remeasure its children, and thus settle the layout process.

[edit] Expanding on this a bit, I can think of a pretty simple solution that only really has one minor downside. You could simply create an AsyncView that extends ViewGroup and, similar to ScrollView, only ever contains a single child which always fills its entire space. The AsyncView does not regard the measurement of its child for its own size, and ideally just fills the available space. All that AsyncView does is wrap the measurement call of its child in a separate thread, that calls back to the view as soon as the measurement is done.

Inside of that view, you can pretty much put whatever you want, including other layouts. It doesn't really matter how deep the "problematic view" is in the hierarchy. The only downside would be that none of the descendants would get rendered until all of the descendants have been measured. But you probably would want to show some kind of loading animation until the view is ready anyways.

The "problematic view" would not need to concern itself with multithreading in any way. It can measure itself just like any other view, taking as much time as it needs.

[edit2] I even bothered to hack together a quick implementation:

package com.example.asyncview;

import android.content.Context;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class AsyncView extends ViewGroup {
    private AsyncTask<Void, Void, Void> mMeasureTask;
    private boolean mMeasured = false;

    public AsyncView(Context context) {
        super(context);
    }

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

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        for(int i=0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.layout(0, 0, child.getMeasuredWidth(), getMeasuredHeight());
        }
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if(mMeasured)
            return;
        if(mMeasureTask == null) {
            mMeasureTask = new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... objects) {
                    for(int i=0; i < getChildCount(); i++) {
                        measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
                    }
                    return null;
                }

                @Override
                protected void onPostExecute(Void aVoid) {
                    mMeasured = true;
                    mMeasureTask = null;
                    requestLayout();
                }
            };
            mMeasureTask.execute();
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if(mMeasureTask != null) {
            mMeasureTask.cancel(true);
            mMeasureTask = null;
        }
        mMeasured = false;
        super.onSizeChanged(w, h, oldw, oldh);
    }
}

See https://github.com/wetblanket/AsyncView for a working example

Timo Ohr
  • 7,947
  • 2
  • 30
  • 20
  • This is very useful. It constrains where the custom view can be used (only in a layout that is aware of what it is dealing with). It can also be quite complicated if the "aware layout" is not the immediate parent of the custom layout; however, I think that can be dealt with. It also simplifies the issue of when to launch the measurement calculation thread. A down-side, though, is that stock layout classes generally cannot be used. Lots of food for thought here. Thanks. – Ted Hopp Feb 07 '13 at 18:58
  • I expanded on the answer a bit – Timo Ohr Feb 07 '13 at 23:01
1

A general approach to expensive calculation is memoization -- caching the results of a calculation in the hope that those results can be used again. I don't know how well memoization applies here, because I don't know if the same input numbers can occur multiple times.

Mark Leighton Fisher
  • 5,609
  • 2
  • 18
  • 29
  • Yes, memoization would be vital in this case. The code I posted actually uses memoization (the `mLayoutHeight` variable), although I didn't refer to the technique by name. – Ted Hopp Feb 09 '13 at 23:54
0

You Can also do Something Like this strategy:

Create Custom Child View:

public class CustomChildView extends View
{
    MyOnResizeListener orl = null; 
    public CustomChildView(Context context)
    {
        super(context);
    }
    public CustomChildView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }
    public CustomChildView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    public void SetOnResizeListener(MyOnResizeListener myOnResizeListener )
    {
        orl = myOnResizeListener;
    }

    @Override
    protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld)
    {
        super.onSizeChanged(xNew, yNew, xOld, yOld);

        if(orl != null)
        {
            orl.OnResize(this.getId(), xNew, yNew, xOld, yOld);
        }
    }
 }

And create some Custom listener like :

public class MyOnResizeListener
 {
    public MyOnResizeListener(){}

    public void OnResize(int id, int xNew, int yNew, int xOld, int yOld){}
 }

You instantiate the listener like:

Class MyActivity extends Activity
{
      /***Stuff***/

     MyOnResizeListener orlResized = new MyOnResizeListener()
     {
          @Override
          public void OnResize(int id, int xNew, int yNew, int xOld, int yOld)
          {
/***Handle resize event and call your measureHeight(int heightMeasureSpec, int width) method here****/
          }
     };
}

And don't forget to pass your listener to your custom view:

 /***Probably in your activity's onCreate***/
 ((CustomChildView)findViewById(R.id.customChildView)).SetOnResizeListener(orlResized);

Finally, you can add your CustomChildView to an XML layout by doing something like:

 <com.demo.CustomChildView>
      <!-- Attributes -->
 <com.demo.CustomChildView/>
Shridutt Kothari
  • 7,326
  • 3
  • 41
  • 61
  • I'm not sure how this helps. I'm not concerned with reacting to a size change in the custom layout. The issue is how to set the _measured_ size of the custom view (something that must happen as part of the layout process--before the size is actually set by the parent of the view). – Ted Hopp Feb 07 '13 at 19:01
0

Android doesn't know the real size at start, it needs to calculate it. Once it's done, onSizeChanged() will notify you with the real size.

onSizeChanged() is called once the size as been calculated. Events don't have to come from users all the time. when android change the size, onSizeChanged() is called. And Same thing with onDraw(), when the view should be drawn onDraw() is called.

onMeasure() is called automatically right after a call to measure()

Some other related link for you

By the way,Thanks for asking such an interesting question here. Happy to help.:)

Shridutt Kothari
  • 7,326
  • 3
  • 41
  • 61
0

There are OS-provided widgets, OS-provided event/callback system, OS-provided layout/constraint management, OS-provided data binding to widgets. Alltogether I call it OS-provided UI API. Regardless of quality of OS-provided UI API, your app sometimes may not be effectively solved using it's features. Because they might have been designed with different ideas in mind. So there is always a good option to abstract, and create your own - your task oriented UI API on top of the one that OS provides you. Your own self provided api would let you calculate and store and then use different widget sizes in more efficient way. Reducing or removing bottlenecks, inter-threading, callbacks etc. Sometimes creating such layer-api is matter of minutes, or sometimes it can be advanced enough to be the largest part of your whole project. Debugging and supporting it over long periods of time may take some effort, this is main disadvantage of this approach. But advantage is that your mind set changes. You start thinking "hot to create" instead of "how to find the way around limitations". I do not know about "best practice" but this is probably one of the most used approach. Often you can hear "we use our own". Choosing between self-made and big-guys provided is not easy. Keeping it sane is important.

exebook
  • 32,014
  • 33
  • 141
  • 226