Friday, July 12, 2013

Dynamic resize of views and offscreen view size calculation in LinearLayout

The layout is usually handled by a set of xml files, where we specify how the components of our view group are displayed.
It should be the preferred way of generating and composing the views. It is possible to merge layouts, to inflate custom views etc.
There are some occasions, though, where the xml syntax and attributes are not powerful enough.
For example, we can have a vertical linear layout where, at some point, we inflate a custom view. We don't know how big (and, above all), tall is this view at compile time. The problem is, all or some views below it can disappear because our "big" view takes all available space.

A solution might be to wrap the views in a relative layout. All good, all fine, in that way we can specify that the big view must be below a child, and above another. Then we align the first view to the top, and the last to the bottom.

But the problem is that the height of the relative layout can't be wrapped!
As stated also in the documentation (which should have read before spending a couple of hours in adapting the layout :P)
Note that you cannot have a circular dependency between the size of the RelativeLayout and the position of its children. For example, you cannot have a RelativeLayout whose height is set to WRAP_CONTENT and a child set to ALIGN_PARENT_BOTTOM.
The only way is adjusting the layout at runtime: we must calculate the height of the big view  and if its bottom point is at the end of the layout and there are still views to draw, then we need to calculate their value and resize it accordingly.

A problem is that we can't get simply the height of the offscreen views! Since they are offscreen, Android will give us 0 as the height.

What we can do, is using the class called View.MeasureSpec and calculate the view size like this:

private Pair<Integer,Integer> measureButton(){
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(ViewGroup.LayoutParams.WRAP_CONTENT, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(ViewGroup.LayoutParams.WRAP_CONTENT, View.MeasureSpec.EXACTLY);
mButton.measure(widthMeasureSpec, heightMeasureSpec);
int h = mButton.getMeasuredHeight();
int w = mButton.getMeasuredWidth();
Log.d(TAG, "height of button is :"+h+", width is :"+w);
return new Pair<Integer, Integer>(w,h);
}


Then, we need to find a way to adjust the size of the big view; again, we can't do that in onCreate() or other similar callbacks, as the view is not rendered yet and we will get 0 as height of the view.
We could override the View.onMeasure() method but as it gets called multiple times by the framework, it is not very reliable.
Best thing is adding a ViewTreeObserver and do all the calculations in its callback:

@Override
protected void onCreate(Bundle savedInstanceState) {
//... init code ...
ViewTreeObserver vto = mRootView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListenerImpl());
}
private void resizeBigView(){
int indexOfBigView = mRootView.indexOfChild(mBigView);
int totalNumberOFChildren = mRootView.getChildCount();
int heightOfButton = measureRequiredHeight(mRootView, indexOfBigView + 1, totalNumberOFChildren);
int bigViewOriginalHeight = mBigView.getHeight();
int bigViewBottom = mBigView.getBottom();
int rootViewHeight = mRootView.getHeight();
int pixelsToRemove = 0;
//if big view covers the button (even partially!)
if(bigViewBottom > rootViewHeight - heightOfButton) {
pixelsToRemove = bigViewBottom - (rootViewHeight - heightOfButton);
Log.d(TAG, "pixels to remove: "+pixelsToRemove);
}
//setup view with the same height as before but changing height from match_parent to precise value
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)mBigView.getLayoutParams();
params.height = bigViewOriginalHeight - pixelsToRemove;
mBigView.setLayoutParams(params);
}
//.. in our listener:
public void onGlobalLayout() {
resizeBigView();
if (android.os.Build.VERSION.SDK_INT < 16) // Use old, deprecated method
mRootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
else // Use new method
mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
view raw resize.java hosted with ❤ by GitHub


The code of this example is available here:
https://github.com/nalitzis/TestDynamicViews




3 comments: