Measure all view hierarchy before drawing in Android - android

I have next view hiererchy:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:gravity="center">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#drawable/group_bg"
android:gravity="center_vertical"
android:baselineAligned="false">
<LinearLayout
android:id="#+id/itemplace"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:layout_weight="1" >
<include
android:layout_width="wrap_content"
android:layout_height="wrap_content"
layout="#layout/pin" />
</LinearLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/arrow_black"/>
</LinearLayout>
</LinearLayout>
I need to get itemplace width before this hierarchy will be drawing. I can not use OnLayoutChangeListener because I use API level 7.
I know that I must use measure() and layout() methods to calculate size of my views but I dont know hot it to do.
Root view in my hierarchy must fill all width of screen, so itemplace width depended of parent size, parent paddings and FrameLayout width.
What must I do to get it width?

I have find out way to get view width. I can get it after android measuret it but before drawing using OnPreDrawListener in ViewTreeObserver:
view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
int avaliableWidth = view.getWidth() - view.getPaddingLeft() - view.getPaddingRight();
Log.d("width", String.valueOf(avaliableWidth));
return true;
}
});
From Android documentation:
Callback method to be invoked when the view tree is about to be drawn.
At this point, all views in the tree have been measured and given a
frame. Clients can use this to adjust their scroll bounds or even to
request a new layout before drawing occurs.

This is available since API 1:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
}

Related

Andorid custom view `onMeasure` not setting the desired width and height

I've created an InnerCustomView and although I'm overriding onMeasure, the view height is not set as the desired height.
Here is some context.
I have a CustomView. This view has a container (RelativeLayout) and inside here I have 4 InnerCustomViews plus two guidelines that I need to place in the container. For simplicity, I'm only showing here one of those InnerCustomViews because I can't manage to set this view to my width and height requirements.
This is the CustomView layout relevant information:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/grey">
<ImageView
android:id="#+id/image"
....
/>
<RelativeLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/red"
android:layout_below="#+id/image">
<View
android:id="#+id/guide"
android:layout_width="12dp"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="#android:color/holo_green_dark" />
.......
<...InnerCustomView
android:id="..."
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toEndOf="#id/guide"
android:background="#color/green"
... />
</RelativeLayout>
</RelativeLayout>
In CustomView I'm doing this:
#Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//Based on the container size I calculate the InnerCustomView desiredSize
innerCustomView.measure(MeasureSpec.makeMeasureSpec(desiredSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(desiredSize, MeasureSpec.EXACTLY));
}
The InnerCustomView only has an ImageView and a TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:background="#drawable/grey">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
I have this in InnerCustomView:
public class InnerCustomView extends LinearLayout {
//Default constructors and other stuff.
#Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
//Here I'm loging the values in widthMeasureSpec and heightMeasureSpec and I can see that they are the desiredSize
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
}
If you look at the image bellow, except height, my layout is as expected. The vertical guide (dark green) is in the correct place and the InnerCustomView is at it's right. The problem is that the width and height are not the ones I've passed when I called measure.
Width takes the available space and height also thus taking the whole height of the view (light green)
On the other hand, the InnerCustomView content has the desiredSize (grey).
Any ideia why is this happening?
I've tried to invalidate() the view, requestLayout(), forceLayout() but with no success.

ImageView.getWidth() return same value in landscape and portrait screen

I want to make my ImageView's size is rectangle shaped based on its width and it's done with overiding onGlobalLayout() and put photo.getWidth(). But it is only work in portrait screen, and no in landscape screen.
Portrait is works fine :
In landscape, the image not rectangle shaped
And this is the code :
fragment_productdetail.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal">
<ImageView
android:id="#+id/ivImage0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:scaleType="fitXY"
android:adjustViewBounds="true"
android:src="#drawable/noimage"/>
<ImageView
android:id="#+id/ivImage1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:scaleType="fitXY"
android:adjustViewBounds="true"
android:src="#drawable/noimage"/>
<ImageView
android:id="#+id/ivImage2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:scaleType="fitXY"
android:adjustViewBounds="true"
android:src="#drawable/noimage"/>
<ImageView
android:id="#+id/ivImage3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="3dp"
android:scaleType="fitXY"
android:adjustViewBounds="true"
android:src="#drawable/noimage"/>
</LinearLayout>
FragmentProductDetail.java
ivPhotos[0] = (ImageView) inflatedView.findViewById(R.id.ivImage0);
ivPhotos[1] = (ImageView) inflatedView.findViewById(R.id.ivImage1);
ivPhotos[2] = (ImageView) inflatedView.findViewById(R.id.ivImage2);
ivPhotos[3] = (ImageView) inflatedView.findViewById(R.id.ivImage3);
for (final ImageView photo : ivPhotos) {
photo.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
photo.getViewTreeObserver().removeOnGlobalLayoutListener(this);
photo.getLayoutParams().height = photo.getWidth();
}
});
}
Then i try put Log.i("zihad", ""+photo.getWidth()); inside onGlobalLayout() to get the width in landscape, then it gives me same value of width in portrait. So what should i do to make it works in portrait and landscape?
*sorry for bad english, i ask here also to improve my english
Quick Tip:
I am not sure, if this is what you want. From what I assume, I think you might require a layout with Square ImageView. If you are trying to change the layout after rendering it, you should call photo.requestLayout(); before setting its height.
An alternative solution would be to use a SquareImageView. To achieve this, just create a View which extends ImageView and set the height by overriding its onMeasure like this.
public class MyImageView extends ImageView {
#Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
setMeasuredDimension(width, width);
}
Now in your layout, substitute ImageView for MyImageView(with complete package name).

How to set weight for width and height?

I need to create view dynamical with width and height which are the percent of parent size.So I need to add weight for width and height for one view.
Is it possible?
<LinearLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:weightSum="4"
android:orientation="horizontal">
<ImageView
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_weight="1"
android:gravity="center"
android:id="#+id/imgtype"
android:scaleType="fitCenter"/>
<TextView
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_weight="3"
android:id="#+id/txtsubject"
android:textStyle="bold"
android:textSize="#dimen/lat_sub_text_size"
android:textColor="#android:color/black"/>
<LinearLayout/>
here you will be able to divide the major portion of parent linearlayout's width to the textview and remaining minor part for imageview this is for aligning horizontally the same can be done for vertical as well.Weightsum property of parent decides number of parts it will contain.
For example, I have the below layout:
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/ll_forImage"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="#+id/ll_forList"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
</LinearLayout>
Now, I will set width and height for each View depend on Screen width and Screen Height.
int screenWidth = dpToPx(getResources().getConfiguration().screenWidthDp);
int screenHeight = dpToPx(getResources().getConfiguration().screenHeightDp);
int ratioW = ....;//
int ratioH = ....;//
setLayoutSize(ll_forImage, screenWidth/ratioW , screenHeight/ratioH );
setLayoutSize(ll_forList, screenWidth/ratioW , screenHeight - screenHeight/ratioH );
// You can use setTranslation to translate View to expected position.
with:
private int dpToPx(int dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}
private static void setLayoutSize(View view, int width, int height) {
ViewGroup.LayoutParams params = view.getLayoutParams();
params.width = width;
params.height = height;
view.setLayoutParams(params);
}
Adding layout_weight is a good approach while using LinearLayout.
Remember, layout_weight works only in case of LinearLayout and cannot be used with RelativeLayout.

ImageView with no source does not honor layout, adding source does not repair layout

My base layout is a ListView of LinearLayouts, and each item in the grid is the FrameLayout below. I am downloading an image to the ImageView. Prior to the image being loaded, there is no content and the layout is shrunk to the height of the small ProgressBar (first problem), when I expect it to be 240dp or 120dp. After the image is placed in the view, the layout does not adjust (second problem) and the height remains the shrunken dimension of a small ProgressBar.
loading image code:
#Override
public View getView(View convertView) {
// ...
// set up holder
// ...
new GetPhotoTask().execute(holder.my_tile_image, holder.my_loading, my_media_url);
holder.my_tile_image.setVisibility(View.INVISIBLE);
holder.my_loading.setVisibility(View.VISIBLE);
holder.my_tile_label.setText(activityObject1.track.name);
// ...
}
private final class GetPhotoTask extends AsyncTask<Object, Void, Drawable> {
ImageView iv;
ProgressBar pb;
#Override
protected Drawable doInBackground(Object... params) {
iv = (ImageView) params[0];
pb = (ProgressBar) params[1];
return new BitmapDrawable(getResources(), loadAsset(params[2]).media_url, true));
}
#Override
protected void onPostExecute(Drawable result) {
super.onPostExecute(result);
pb.setVisibility(View.GONE);
iv.setImageDrawable(result);
iv.setVisibility(View.VISIBLE);
}
}
the main xml:
<ListView
android:id="#+id/my_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="2dp" />
the xml for each row:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/my_row_type_3"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="240dp"
android:weightSum="3" >
<include layout="#layout/my_tile_layout"
android:id="#+id/my_tile_1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2" />
<LinearLayout
android:id="#+id/my_row_type_2"
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:weightSum="2" >
<include layout="#layout/my_tile_layout"
android:id="#+id/my_tile_2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<include layout="#layout/my_tile_layout"
android:id="#+id/my_tile_3"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
the xml for row content:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/my_tile_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="2dp" >
<ImageView
android:id="#+id/my_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="#string/foo" />
<ProgressBar
android:id="#+id/my_loading"
style="?android:attr/progressBarStyleSmall"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
<TextView
android:id="#+id/my_tile_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/my_title"
android:layout_gravity="left|bottom" />
</FrameLayout>
Why does the initial layout shrink to the height of progress bar even though I've hard coded 240dp for each row? Why does setting a bitmap in the ImageView content not cause it to resize at that point either? The first question most important, as I believe it will nullify the second. Thanks in advance.
SOLUTION
To get the structure I want, I use RelativeLayout for the tiles (for layering ability), and for each row I am overriding the LinearLayout to enforce the sizing I want set for each row. As there is a potential for each tile's content to be larger or smaller than the view, this is screwing up my ability to rely on either centerCrop or fitCenter. So in my custom layout, I set the height to be proportional to the width of each row, and the mode of the MeasureSpec to be EXACTLY.
public final class MyRow3 extends LinearLayout {
// ...
#Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, makeMeasureSpec(getSize(widthMeasureSpec) * 2 / 3, MeasureSpec.EXACTLY));
}
I'm not sure why it isn't honouring it but why not do something like this instead:
Set an absolute height in your ImageView to guarantee some space.
<ImageView
android:layout_height="###dp"
/>
Then change the layout back to an auto adjusted state just before the image is loaded:
iv.setLayoutParams( new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT ) );
To get the structure I want, I use RelativeLayout for the tiles (for layering ability), and for each row I am overriding the LinearLayout to enforce the sizing I want set for each row. As there is a potential for each tile's content to be larger or smaller than the view, this is screwing up my ability to rely on either centerCrop or fitCenter. So in my custom layout, I set the height to be proportional to the width of each row, and the mode of the MeasureSpec to be EXACTLY.
public final class MyRow3 extends LinearLayout {
// ...
#Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, makeMeasureSpec(getSize(widthMeasureSpec) * 2 / 3, MeasureSpec.EXACTLY));
}

android ScrollView layout (wrap_content, maximum size)

Here is what I would like my ScrollView to look like:
The maximum size is defined with the layout_weight (so that other items below the ScrollView can be displayed properly)
If the content is smaller than that maximum size, then it just behaves as with layout_height="wrap_content"
Here is what I currently have:
<ScrollView
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:measureAllChildren="true"
android:fillViewport="false"
>
I don't think the measureAllChildren really does anything at all...
If I add android:layout_weight, the size will always be what I would like the maximum to be. Without it, it just extends more than it should...
I don't mind extending the ScrollView class to change the behavior of onMeasure if I need to...?
PS: If that makes a differences, I am trying to get this working from Froyo onward.
I ended up writing my own class, extending ScrollView
Since you ask...here is the code. Probably not the cleanest but it does what I want.
Note that it expects the layout_weight to be set when the view is created and you should not set the weigthSum in the parent LinearLayout or you'll get funny things (since the weight of this one changes from the original value to 0 depending on the size of the content of the ScrollView)
First, in the layout file, the view is declared like this:
<com.matthieu.widget.ShrinkingScrollView
android:id="#+id/scroll"
android:scrollbars="vertical"
android:layout_height="0dp"
android:layout_width="fill_parent"
android:layout_weight="4"
android:background="#cc0000"
>
<TextView
android:id="#+id/in_scroll_view"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:background="#0000bb"
/>
</com.matthieu.widget.ShrinkingScrollView>
Then the code for the widget:
import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.ScrollView;
public class ShrinkingScrollView extends ScrollView {
private float original_weight=-1;
public ShrinkingScrollView(Context context) {
super(context);
}
public ShrinkingScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ShrinkingScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams();
float previous_weight = params.weight;
if (original_weight == -1)
original_weight = params.weight;
if ((getChildCount()>0) && (getVisibility()!=GONE)) {
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED));
int overall_height = getChildAt(0).getMeasuredHeight();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getMeasuredHeight() >= overall_height) {
if (previous_weight != 0) {
params.weight=0;
params.height = overall_height;
setLayoutParams(params);
post(new Runnable() {
public void run() {
requestLayout();
}
});
}
setMeasuredDimension(getMeasuredWidth(),overall_height);
}
else if (previous_weight == 0) {
params.weight = original_weight;
params.height = 0;
setLayoutParams(params);
post(new Runnable() {
public void run() {
requestLayout();
}
});
}
}
else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
If I understand your requirement correctly, the way I have done something like this is to place the scrollview along with anything else you want to display below it into a Relative layout. Then, in your layout file place the item on the bottom of the screen in the file first (with android:layout_alignParentBottom="true"), then put the scroll view and place it above the first item using something like:android:layout_above="#id/firstItem". Like this in which I put an image view on the bottom with a scroll view above it taking the remaining space:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/searchByNameScreen"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center_horizontal"
android:layout_marginTop="1dip"
android:gravity="center_horizontal"
android:orientation="vertical" >
<ImageView
android:id="#+id/bottomImage"
android:layout_width="fill_parent"
android:layout_height="75dip"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:paddingBottom="15dip"
android:paddingLeft="10dip"
android:paddingRight="10dip"
android:paddingTop="7dip"
android:scaleType="fitXY"
android:src="#drawable/android_450x50_moreinfo" />
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/ScrollView01"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_above="#id/bottomImage"
android:layout_marginBottom="3dip"
android:layout_marginLeft="10dip"
android:layout_marginRight="10dip"
android:layout_marginTop="1dip"
android:scrollbars="none" >
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/aboutCopyLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="#id/aboutTopLayer"
android:layout_centerHorizontal="true"
android:layout_gravity="center_horizontal"
android:background="#drawable/rounded_background"
android:gravity="center"
android:padding="5dip" >
<!-- android:background="#drawable/rounded_background" -->
<TextView
android:id="#+id/aboutCopyText"
style="#style/ResortNameExtraLine"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center_horizontal"
android:text="#string/aboutCopy" />
</LinearLayout>
</ScrollView>
</RelativeLayout>
Here another solution using sizes instead of weights. I've extended ScrollView and added code to implement this feature:
https://gist.github.com/JMPergar/439aaa3249fa184c7c0c
I hope that be useful.
JMpergar answer seems right but its not worked until I changed the onMeasure method like below.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
try {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (maxHeight != WITHOUT_MAX_HEIGHT_VALUE
&& heightSize > maxHeight) {
heightSize = maxHeight;
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST);
getLayoutParams().height = heightSize;
} else {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST);
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
, getDefaultSize(this.getSuggestedMinimumHeight(), heightMeasureSpec));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
you can just wrap your ScrollView into another ConstraintLayout, then constrain the height (the line app:layout_constrainedHeight="true"):
<!-- another constraint layout to keep ScrollView shrinking -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constrainedHeight="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/content_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">

Categories

Resources