How would I go implementing a fixed aspect ratio View? I'd like to have items with 1:1 aspect ratio in a GridView. I think it's better to subclass the children than the GridView?
EDIT: I assume this needs to be done programmatically, that's no problem. Also, I don't want to limit the size, only the aspect ratio.
I implemented FixedAspectRatioFrameLayout, so I can reuse it and have any hosted view be with fixed aspect ratio:
public class FixedAspectRatioFrameLayout extends FrameLayout
{
private int mAspectRatioWidth;
private int mAspectRatioHeight;
public FixedAspectRatioFrameLayout(Context context)
{
super(context);
}
public FixedAspectRatioFrameLayout(Context context, AttributeSet attrs)
{
super(context, attrs);
init(context, attrs);
}
public FixedAspectRatioFrameLayout(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs)
{
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FixedAspectRatioFrameLayout);
mAspectRatioWidth = a.getInt(R.styleable.FixedAspectRatioFrameLayout_aspectRatioWidth, 4);
mAspectRatioHeight = a.getInt(R.styleable.FixedAspectRatioFrameLayout_aspectRatioHeight, 3);
a.recycle();
}
// **overrides**
#Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
{
int originalWidth = MeasureSpec.getSize(widthMeasureSpec);
int originalHeight = MeasureSpec.getSize(heightMeasureSpec);
int calculatedHeight = originalWidth * mAspectRatioHeight / mAspectRatioWidth;
int finalWidth, finalHeight;
if (calculatedHeight > originalHeight)
{
finalWidth = originalHeight * mAspectRatioWidth / mAspectRatioHeight;
finalHeight = originalHeight;
}
else
{
finalWidth = originalWidth;
finalHeight = calculatedHeight;
}
super.onMeasure(
MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY));
}
}
For new users, here's a better non-code solution :
A new support library called Percent Support Library is available in Android SDK v22 (MinAPI is 7 me thinks, not sure) :
src : android-developers.blogspot.in
The Percent Support Library provides percentage based dimensions and margins and, new to this release, the ability to set a custom aspect ratio via app:aspectRatio. By setting only a single width or height and using aspectRatio, the PercentFrameLayout or PercentRelativeLayout will automatically adjust the other dimension so that the layout uses a set aspect ratio.
To include add this to your build.gradle :
compile 'com.android.support:percent:23.1.1'
Now wrap your view (the one that needs to be square) with a PercentRelativeLayout / PercentFrameLayout :
<android.support.percent.PercentRelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
app:layout_aspectRatio="100%"
app:layout_widthPercent="100%"/>
</android.support.percent.PercentRelativeLayout>
You can see an example here.
To not use third-party solution and considering the fact that both PercentFrameLayout and PercentRelativeLayout were deprecated in 26.0.0, I'd suggest you to consider using ConstraintLayout as a root layout for your grid items.
Your item_grid.xml might look like:
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/imageview_item"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="H,1:1" />
</android.support.constraint.ConstraintLayout>
As a result you get something like this:
I recently made a helper class for this very problem and wrote a blog post about it.
The meat of the code is as follows:
/**
* Measure with a specific aspect ratio<br />
* <br />
* #param widthMeasureSpec The width <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method
* #param heightMeasureSpec The height <tt>MeasureSpec</tt> passed in your <tt>View.onMeasure()</tt> method
* #param aspectRatio The aspect ratio to calculate measurements in respect to
*/
public void measure(int widthMeasureSpec, int heightMeasureSpec, double aspectRatio) {
int widthMode = MeasureSpec.getMode( widthMeasureSpec );
int widthSize = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( widthMeasureSpec );
int heightMode = MeasureSpec.getMode( heightMeasureSpec );
int heightSize = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize( heightMeasureSpec );
if ( heightMode == MeasureSpec.EXACTLY && widthMode == MeasureSpec.EXACTLY ) {
/*
* Possibility 1: Both width and height fixed
*/
measuredWidth = widthSize;
measuredHeight = heightSize;
} else if ( heightMode == MeasureSpec.EXACTLY ) {
/*
* Possibility 2: Width dynamic, height fixed
*/
measuredWidth = (int) Math.min( widthSize, heightSize * aspectRatio );
measuredHeight = (int) (measuredWidth / aspectRatio);
} else if ( widthMode == MeasureSpec.EXACTLY ) {
/*
* Possibility 3: Width fixed, height dynamic
*/
measuredHeight = (int) Math.min( heightSize, widthSize / aspectRatio );
measuredWidth = (int) (measuredHeight * aspectRatio);
} else {
/*
* Possibility 4: Both width and height dynamic
*/
if ( widthSize > heightSize * aspectRatio ) {
measuredHeight = heightSize;
measuredWidth = (int)( measuredHeight * aspectRatio );
} else {
measuredWidth = widthSize;
measuredHeight = (int) (measuredWidth / aspectRatio);
}
}
}
I created a layout library using TalL's answer. Feel free to use it.
RatioLayouts
Installation
Add this to the top of the file
repositories {
maven {
url "http://dl.bintray.com/riteshakya037/maven"
}
}
dependencies {
compile 'com.ritesh:ratiolayout:1.0.0'
}
Usage
Define 'app' namespace on root view in your layout
xmlns:app="http://schemas.android.com/apk/res-auto"
Include this library in your layout
<com.ritesh.ratiolayout.RatioRelativeLayout
android:id="#+id/activity_main_ratio_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fixed_attribute="WIDTH" // Fix one side of the layout
app:horizontal_ratio="2" // ratio of 2:3
app:vertical_ratio="3">
Update
With introduction of ConstraintLayout you don't have to write either a single line of code or use third-parties or rely on PercentFrameLayout which were deprecated in 26.0.0.
Here's the example of how to keep 1:1 aspect ratio for your layout using ConstraintLayout:
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:layout_marginTop="0dp"
android:background="#android:color/black"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</LinearLayout>
</android.support.constraint.ConstraintLayout>
I've used and liked Jake Wharton's implementation of ImageView (should go similarly for other views), others might enjoy it too:
AspectRatioImageView.java - ImageView that respects an aspect ratio applied to a specific measurement
Nice thing it's styleable in xml already.
Simply override onSizeChanged and calculate ratio there.
Formula for aspect ratio is:
newHeight = original_height / original_width x new_width
this would give you something like that:
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//3:5 ratio
float RATIO = 5/3;
setLayoutParams(new LayoutParams((int)RATIO * w, w));
}
hope this helps!
The ExoPlayer from Google comes with an AspectRatioFrameLayout that you use like this:
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:resize_mode="fixed_width">
<!-- https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.html#RESIZE_MODE_FIXED_WIDTH -->
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
Then you must set the aspect ratio programmatically:
aspectRatioFrameLayout.setAspectRatio(16f/9f)
Note that you can also set the resize mode programmatically with setResizeMode.
Since you are obviously not going to grab the whole ExoPlayer library for this single class, you can simply copy-paste the file from GitHub to your project (it's open source):
https://github.com/google/ExoPlayer/blob/release-v2/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java
Don't forget to grab the attribute resize_mode too:
https://github.com/google/ExoPlayer/blob/release-v2/library/ui/src/main/res/values/attrs.xml#L18-L25
<attr name="resize_mode" format="enum">
<enum name="fit" value="0"/>
<enum name="fixed_width" value="1"/>
<enum name="fixed_height" value="2"/>
<enum name="fill" value="3"/>
<enum name="zoom" value="4"/>
</attr>
You may find third-party libraries. Instead of using them, use constraint layout.
Below code sets the aspect ratio of ImageView as 16:9 regardless of the screen size and orientation.
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ImageView
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="fitXY"
android:src="#drawable/mat3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintDimensionRatio="H,16:9"/>
</androidx.constraintlayout.widget.ConstraintLayout>
app:layout_constraintDimensionRatio="H,16:9". Here, height is set with respect to the width of the layout.
For your question, set android:layout_width="match_parent" and use app:layout_constraintDimensionRatio="H,1:1" in your view.
I have used in this way.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/imageView_home_one"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="H,4:3"
android:layout_marginTop="#dimen/_5sdp"
android:visibility="gone"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
Related
I stumbled across this problem when working with custom Square Layout : by extending the Layout and overriding its onMeasure() method to make the dimensions = smaller of the two (height or width).
Following is the custom Layout code :
public class CustomSquareLayout extends RelativeLayout{
public CustomSquareLayout(Context context) {
super(context);
}
public CustomSquareLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomSquareLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CustomSquareLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//Width is smaller
if(widthMeasureSpec < heightMeasureSpec)
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
//Height is smaller
else
super.onMeasure(heightMeasureSpec, heightMeasureSpec);
}
}
The custom Square Layout works fine, until in cases where the custom layout goes out of bound of the screen. What should have automatically adjusted to screen dimensions though, doesn't happen. As seen below, the CustomSquareLayout actually extends below the screen (invisible). What I expect is for the onMeasure to handle this, and give appropriate measurements. But that is not the case. Note of interest here is that even thought the CustomSquareLayout behaves weirdly, its child layouts all fall under a Square shaped layout that is always placed on the Left hand side.
<!-- XML for above image -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="300dp"
android:text="Below is the Square Layout"
android:gravity="center"
android:id="#+id/text"
/>
<com.app.application.CustomSquareLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#id/text"
android:background="#color/colorAccent" #PINK
android:layout_centerInParent="true"
android:id="#+id/square"
android:padding="16dp"
>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" #Note this
android:background="#color/colorPrimaryDark" #BLUE
>
</RelativeLayout>
</com.app.application.CustomSquareLayout>
</RelativeLayout>
Normal case : (Textview is in Top)
Following are few links I referenced:
Custom Square LinearLayout. How?
Simple way to do dynamic but square layout
Hope to find a solution to this, using onMeasure or any other function when extending the layout (so that even if some extends the Custom Layout, the Square property remains)
Edit 1 : For further clarification, the expected result for 1st case is shown
Edit 2 : I gave a preference to onMeasure() or such functions as the need is for the layout specs (dimensions) to be decided earlier (before rendering). Otherwise changing the dimensions after the component loads is simple, but is not requested.
You can force a square view by checking for "squareness" after layout. Add the following code to onCreate().
final View squareView = findViewById(R.id.square);
squareView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
squareView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
if (squareView.getWidth() != squareView.getHeight()) {
int squareSize = Math.min(squareView.getWidth(), squareView.getHeight());
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) squareView.getLayoutParams();
lp.width = squareSize;
lp.height = squareSize;
squareView.requestLayout();
}
}
});
This will force a remeasurement and layout of the square view with a specified size that replaces MATCH_PARENT. Not incredibly elegant, but it works.
You can also add a PreDraw listener to your custom view.
onPreDraw
boolean onPreDraw ()
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.
Return true to proceed with the current drawing pass, or false to cancel.
Add a call to an initialization method in each constructor in the custom view:
private void init() {
this.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
if (getWidth() != getHeight()) {
int squareSize = Math.min(getWidth(), getHeight());
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) getLayoutParams();
lp.width = squareSize;
lp.height = squareSize;
requestLayout();
return false;
}
return true;
}
});
}
The XML can look like the following:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="300dp"
android:gravity="center"
android:text="Below is the Square Layout" />
<com.example.squareview.CustomSquareLayout
android:id="#+id/square"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#id/text"
android:background="#color/colorAccent"
android:padding="16dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="#color/colorPrimaryDark" />
</com.example.squareview.CustomSquareLayout>
</RelativeLayout>
There is a difference between the view's measured width and the view's width (same for height). onMeasure is only setting the view's measured dimensions. There is still a different part of the drawing process that constrains the view's actual dimensions so that they don't go outside the parent.
If I add this code:
final View square = findViewById(R.id.square);
square.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
System.out.println("measured width: " + square.getMeasuredWidth());
System.out.println("measured height: " + square.getMeasuredHeight());
System.out.println("actual width: " + square.getWidth());
System.out.println("actual height: " + square.getHeight());
}
});
I see this in the logs:
09-05 10:19:25.768 4591 4591 I System.out: measured width: 579
09-05 10:19:25.768 4591 4591 I System.out: measured height: 579
09-05 10:19:25.768 4591 4591 I System.out: actual width: 768
09-05 10:19:25.768 4591 4591 I System.out: actual height: 579
How to solve it by creating a custom view? I don't know; I never learned. But I do know how to solve it without having to write any Java code at all: use ConstraintLayout.
ConstraintLayout supports the idea that children should be able to set their dimensions using an aspect ratio, so you can simply use a ratio of 1 and get a square child. Here's my updated layout (the key piece is the app:layout_constraintDimensionRatio attr):
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="300dp"
android:gravity="center"
android:text="Below is the Square Layout"
app:layout_constraintTop_toTopOf="parent"/>
<RelativeLayout
android:id="#+id/square"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="16dp"
android:background="#color/colorAccent"
app:layout_constraintTop_toBottomOf="#+id/text"
app:layout_constraintDimensionRatio="1">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:background="#color/colorPrimaryDark">
</RelativeLayout>
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
And screenshots:
You cannot compare the two measure specs, as they are not simply a size. You can see a very good explanation in this answer. This answer is for a custom view, but measure specs are the same. You need to get the mode and the size to compute final sizes, and compare the end results for both dimensions.
In the second example you shared, the right question is this one (third answer). Is written for Xamarin in C#, but is easy to understand.
The case that is failing for you is because you're finding an AT_MOST mode (when the view is hitting the bottom of the screen), that's why comparisons are failing in this case.
That should be the final method (can contain typos, I have been unable to test it:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
switch (widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
width = Math.min(widthSize, heightSize);
break;
default:
width = 100;
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
height = Math.min(widthSize, heightSize);
break;
default:
height = 100;
break;
}
var size = Math.min(width, height);
var newMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
super.onMeasure(newMeasureSpec, newMeasureSpec);
}
I expect the end result to be roughly like this (maybe centered, but this dimensions):
Notice that this is a made up image done with Gimp.
try this. You can use on measure method to make a custom view. Check the link below for more details.
http://codecops.blogspot.in/2017/06/how-to-make-responsive-imageview-in.html
Url image having width 900*346, we are using same image for displaying detail image and thumbnail image.Thumbnail image is displaying in Gridview with 2 columns and 200dp height but image is stretching.Is there any way to display larger width image with out stretch. Thanks in advance.
GridView Xml:
<?xml version="1.0" encoding="utf-8"?>
<GridView
android:id="#+id/grid_img"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:horizontalSpacing="#dimen/portfolio_grid_space"
android:verticalSpacing="#dimen/portfolio_grid_space"
android:layout_marginTop="#dimen/portfolio_grid_space"
android:layout_marginBottom="#dimen/portfolio_grid_space"
android:numColumns="2"/>
Gridview Adapter xml:
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/img"
android:scaleType="fitCenter"
android:layout_width="match_parent" android:layout_height="200dp">
</ImageView>
Give in the xml file of your layout android:scaleType="fitXY"
P.S : this applies to when the image is set with android:src="..." rather than android:background="..." as backgrounds are set by default to stretch and fit to the View.
If I do not misunderstand, you want to keep image width/height ratio while stretching the image. You can put the below lines in your xml ImageView:
android:adjustViewBounds="true"
android:scaleType="centerCrop"
You can play around with scaleType to get your desired image.
IF you are using device below API 17, you can custom ImageView:
public class ScaleAspectFillImageView extends ImageView {
public ScaleAspectFillImageView(Context context) {
super(context);
}
public ScaleAspectFillImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScaleAspectFillImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// If API level <= 17,
// Only need custom measurement for image whose size is smaller than imageView size.
// http://developer.android.com/reference/android/widget/ImageView.html#setAdjustViewBounds(boolean)
if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
float viewWidth = MeasureSpec.getSize(widthMeasureSpec);
float viewHeight = MeasureSpec.getSize(heightMeasureSpec);
// Calculate imageView.height based on imageView.width and drawable aspect ratio using scale-aspect-fill mode.
Drawable drawable = getDrawable();
if (drawable != null) {
float drawableWidth = drawable.getIntrinsicWidth();
float drawableHeight = drawable.getIntrinsicHeight();
// Only need custom measurement for image whose size is smaller than imageView size.
// Avoid divide by zero error.
boolean needCustomMeasurement = drawableWidth < viewWidth || drawableHeight < viewHeight;
if (needCustomMeasurement && drawableHeight > 0) {
float drawableAspectRatio = drawableWidth / drawableHeight;
// imageView.width and drawable aspect ratio must be greater than zero.
if (drawableAspectRatio > 0 && viewWidth > 0) {
float targetHeight = viewWidth / drawableAspectRatio;
setMeasuredDimension((int) viewWidth, (int) targetHeight);
return; // Already set the measured dimension.
}
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
I would like to have a square (same width as height) GridView fill the full height of the screen in landscape orientation. The Gridview is a chessboard (8 by 8 squares) with the xml:
<com.example.jens.jchess2.view.MyGridView
android:id="#+id/chessboard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="0dp"
android:numColumns="8"
android:verticalSpacing="0dp"
android:horizontalSpacing="0dp">
</com.example.jens.jchess2.view.MyGridView>
and the elements of the grid are:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/square"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000080"
android:orientation="vertical"
android:padding="0pt">
<ImageView
android:id="#+id/square_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:padding="0pt" />
<ImageView
android:id="#+id/piece"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:padding="0pt" />
</FrameLayout>
, where the ImageViews correspond to the squares and pieces (both from png images) of the board.
In the custom MyGridView I override onMeasure as follows:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = getContext().getResources().getDisplayMetrics().heightPixels;
if (width > height) {
super.onMeasure(
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);
} else {
super.onMeasure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
);
}
}
which gives me a square GridView in both portrait and landscape orientation. In portrait mode it fills the full width and everything is fine. In landscape mode however it extends below the screen because the height (=width) of the GridView/board is too large. It is too large by the height of the toolbar and the height of the statusbar. How can I get the proper size for the GridView, i.e. screen height minus status bar height minus toolbar height?
Start with two versions of your layout file:
/res/layout/grid.xml
...
<!-- full width -->
<com.example.MyGridView
android:layout_width="match_parent"
android:layout_height="wrap_content"
...
/>
...
/res/layout-land/grid.xml
...
<!-- full height -->
<com.example.MyGridView
android:layout_width="wrap_content"
android:layout_height="match_parent"
...
/>
...
You probably already have something like this.
Now in your onMeasure() override, the match_parent dimension will have a MeasureSpec mode of EXACTLY and the wrap_content dimension will have a MeasureSpec mode of AT_MOST. You can use this to achieve your desired layout.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.AT_MOST) {
// portrait
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
} else if (heightMode == MeasureSpec.EXACTLY && widthMode == MeasureSpec.AT_MOST) {
// landscape
super.onMeasure(heightMeasureSpec, heightMeasureSpec);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
EDIT: I found out that both modes can be AT_MOST depending on the ViewGroup container. Please see my other answer for updated measuring code.
Ah. Now I see that this is for a game.
Sometimes it's better to have layouts and child views, but in most cases with game boards you are better off creating a single View subclass that represents the game view.
For instance, what if your users say they want the ability to pinch-zoom into one quadrant of the game board? You can't do that with a GridView.
I whipped up a simple app to show you how this can work. I simplified the onMeasure() code I posted before, and instead of a GridView, a single View subclass renders the game board.
The MainActivity simply sets up the content view.
/res/layout/activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.example.gameboard.MainActivity">
<com.example.gameboard.GameBoardView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
/res/layout-land/activity_main.xml:
Notice match_parent and wrap_content are switched for width and height.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context="com.example.gameboard.MainActivity">
<com.example.gameboard.GameBoardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true" />
</RelativeLayout>
GameBoardView.java:
public class GameBoardView extends View {
private Paint mPaint;
public GameBoardView(Context context) {
super(context);
}
public GameBoardView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public GameBoardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public GameBoardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(width, height);
int sizeMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
super.onMeasure(sizeMeasureSpec, sizeMeasureSpec);
mPaint = new Paint();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int w = getWidth() / 8;
int h = getHeight() / 8;
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
// choose black or white depending on the square
mPaint.setColor((row + col) % 2 == 0 ? 0xFFFFFFFF : 0xFF000000);
canvas.drawRect(w * col, h * row, w * (col + 1), h * (row + 1), mPaint);
}
}
}
}
Here I'm just drawing the squares right in the view. Now, if I were making a chess game, I would also create a Drawable subclass that would take the game model and render it. Having a separate Drawable for rendering the game makes it easy to scale to the correct size. For example, your Drawable could render at a fixed constant size, then be scaled by the View subclass to fit. The View subclass would function mostly as a controller, interpreting touch events and updating the game model.
I want the width of an ImageView to be set by the parent and the height should be aspect proportional. The reasons for this is that next is shown a TextView that I want to place just under the ImageView.
I can get image to show correctly using
android:layout_width="fill_parent"
android:layout_height="fill_parent"
however the ImageView height become the parent height which is much larger than that of the shown stretched image. One idea was to make parent smaller vertically, but.. there I don't yet know the stretched image size.
The following doesn't work because a small image is not filled up horizontally.
android:layout_width="fill_parent"
android:layout_height="wrap_content"
Messing around with
android:layout_height="wrap_content"
for the RelativeLayout surrounding it all does not help. Also tried FrameLayout and LinearLayout and failed.
Any ideas?
You have to set adjustViewBounds to true.
imageView.setAdjustViewBounds(true);
There is two case if your actual image size is equal or grater than your ImageView width and heigh then you can use adjustViewBounds property and if your actual image size is less than ImageView width and height than use scaleType property to shown image in ImageView based on your requirement.
1.Actual image size is equal or grater than ImageView required width and height.
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="#drawable/ic_launcher"/>
2.Actual image size is less than ImageView required width and height.
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:src="#drawable/ic_launcher"/>
So I have had the same issue more than once and looking through the existing stackoverflow answers have realised that no answer gives a perfect explanation for the real confusion regarding a solution to this problem. So here you go:
API 17+
imageView.setAdjustViewBounds(true);
OR (in XML)
android:adjustViewBounds="true"
solves the issue, it works no matter the actual size of the image resource, i.e. it will scale your image up or down to the desired size you have in your layout.
Below API 17
Below API 17 android:adjustViewBounds="true" will only work for shrinking an image, not growing, i.e. if the actual height of the image source is smaller than the dimensions you are trying to achieve in your layout, wrap_content will use that smaller height and not scale 'up' (enlarge) the image as you desire.
And so for API 17 and lower, you have no choice but to use a custom ImageView to achieve this behaviour. You could either write a custom ImageView yourself or use a library, that has already done that job.
Using a library
There is probably more than one library that fixes this issue, one of them is:
compile 'com.inthecheesefactory.thecheeselibrary:adjustable-imageview:1.0.0'
which is used like this:
<com.inthecheesefactory.thecheeselibrary.widget.AdjustableImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="#drawable/your_drawable"/>
Using a custom View
alternatively to using an existing library, you could write a custom view yourself, e.g.:
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ImageView;
public class ScalableImageView extends ImageView {
boolean adjustViewBounds;
public ScalableImageView(Context context) {
super(context);
}
public ScalableImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScalableImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public void setAdjustViewBounds(boolean adjustViewBounds) {
this.adjustViewBounds = adjustViewBounds;
super.setAdjustViewBounds(adjustViewBounds);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Drawable drawable = getDrawable();
if (drawable == null) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
if (adjustViewBounds) {
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY && widthMode != MeasureSpec.EXACTLY) {
int height = heightSize;
int width = height * drawableWidth / drawableHeight;
if (isInScrollingContainer())
setMeasuredDimension(width, height);
else
setMeasuredDimension(Math.min(width, widthSize), Math.min(height, heightSize));
} else if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
int width = widthSize;
int height = width * drawableHeight / drawableWidth;
if (isInScrollingContainer())
setMeasuredDimension(width, height);
else
setMeasuredDimension(Math.min(width, widthSize), Math.min(height, heightSize));
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private boolean isInScrollingContainer() {
ViewParent parent = getParent();
while (parent != null && parent instanceof ViewGroup) {
if (((ViewGroup) parent).shouldDelayChildPressedState()) {
return true;
}
parent = parent.getParent();
}
return false;
}
}
... which you would use as follows (XML):
<com.YOUR_PACKE_NAME.ScalableImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="#drawable/your_drawable" />
This was the only way I could get it working, to have the second ImageView in the center and smaller than the first ImageView, and the TextView under the second ImageView.
Unfortunately, it uses fixed "200dp" image size on the second ImageView, so it does not look the same on different sized devices.
It also destroys my ViewFlipper, since any Layout I tried around the second ImageView and the TextView makes them move or resize.
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<ImageView
android:id="#+id/imageview1"
android:contentDescription="#string/desc"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="#drawable/background" />
<ImageView
android:id="#+id/imageview2"
android:layout_centerInParent="true"
android:contentDescription="#string/desc"
android:layout_height="200dp"
android:layout_width="200dp"
android:adjustViewBounds="true"
android:src="#drawable/image2" />
<TextView
android:id="#+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="#id/imageview2"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:gravity="center"
android:textSize="26sp"
android:textColor="#333"
android:background="#fff"
android:text="this is the text under the image right here"
/>
</RelativeLayout>
I create my own class for the square layout:
public class SquareLayout extends LinearLayout{
public SquareLayout(Context context) {
super(context);
}
public SquareLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SquareLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = width > height ? height : width;
setMeasuredDimension(size, size);
}
Then, in my xml:
...
<com.myApp.SquareLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center_horizontal"
android:orientation="horizontal" >
<ImageView
android:id="#+id/cellImageView"
android:adjustViewBounds="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:padding="2dp"
android:scaleType="centerCrop"
android:src="#drawable/image" />
</com.myApp.SquareLayout>
...
Nothing written more in my java code.
But instead if my layout and my Image, I see only a white rectangle...
What am I wrong?
// you forget to call super.onMeasure(widthMeasureSpec, widthMeasureSpec);
#Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = width > height ? height : width;
setMeasuredDimension(size, size);
}
// xml file
<com.example.testapplication.SquareLayout
android:id="#+id/layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center_horizontal"
android:orientation="horizontal" >
<ImageView
android:id="#+id/cellImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:adjustViewBounds="true"
android:padding="2dp"
android:scaleType="centerCrop"
android:src="#drawable/ic_launcher" />
</com.example.testapplication.SquareLayout>
I had problems calling setMeasuredDimension directly when applying the same technique to a RelativeLayout. I was unable to correctly align against the bottom edge. Changing to instead call up to super.onMeasure() with a new measure spec worked better.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(width, height);
super.onMeasure(MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY));
}
Edit:
The solution below has now been deprecated, as ConstraintLayout has become the new standard and provides this functionality.
Original Answer:
Turns out the Android team gave us the solution, but nobody knows about it! Check out these two classes from the Percent Support Library:
PercentFrameLayout
PercentRelativeLayout
If you want to impose the ratio of a view, you have to place it within one of these layouts. So in this case, you have to place a standard LinearLayout, not your subclass, within one of these layouts with the right aspect ratio. Example if you want to use a PercentFrameLayout:
<android.support.percent.PercentFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_aspectRatio="100%">
<!-- Whatever subviews you want here -->
</LinearLayout>
</android.support.percent.PercentFrameLayout>
And there you go, all your views will be contained within a square linear
layout!
don't forget to add the gradle dependency compile 'com.android.support:percent:23.3.0' Adjust the version number as required