How should I go about allocating fixed space for adaptive ad banners?
My XML adaptive banner
<FrameLayout
android:id="#+id/ad_view_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_alignParentBottom="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
Inside my Fragment getting adaptive ad banner dimensions
private val adSize: AdSize
get() {
val outMetrics = Resources.getSystem().displayMetrics;
val density = outMetrics.density
var adWidthPixels = adContainerView.width.toFloat()
if (adWidthPixels == 0f) {
adWidthPixels = outMetrics.widthPixels.toFloat()
}
val adWidth = (adWidthPixels / density).toInt()
return AdSize.getCurrentOrientationBannerAdSizeWithWidth(context, adWidth)
}
I would like to follow Googles guidlines and have an allocated fixed space reserved for my banner, so that if my banner "lags" for a second screen on mobile stays the same.
How should I go about it, whats the proper way?
when starting the app you can get the adaptive banner height straight away by calling something like adSize.getHeight(). This will give you banner height in dps.
You then convert that heigth dps in px and set this height to your ad frame layout programatically by calling setLayoutParams with acquired height in pixels.
This way you dont need to wait for the ad to load to apply proper height and risk ad overlapping policy violation or conent shifting (which would happen if you depend on wrap_content)
If you have a more complex behavior, like I do, you can use the following implementation. I have a ConstraintLayout that splits my screen in two parts in portrait mode. The banner is within one of these two parts and the ratio between them is not fixed but depending on some logic. So this implementation also works with that requirement, as it overwrites onMeasure to determine the best size depending on the available width.
public class AdBanner extends FrameLayout
{
private AdView mAdView;
private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
public AdBanner(Context context)
{
super(context);
}
public AdBanner(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public AdBanner(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
if (!isShowBanner())
{
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.AT_MOST));
return;
}
int width = MeasureSpec.getSize(widthMeasureSpec);
if (width <= 0)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// That's where we determine the most accurate banner format.
AdSize adSize = AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(getContext(), getDpWidth(width));
int height = adSize.getHeightInPixels(getContext());
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != MeasureSpec.UNSPECIFIED)
{
height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
}
setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)));
}
protected int getDpWidth(int width)
{
Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
display.getMetrics(mDisplayMetrics);
return (int) (width / mDisplayMetrics.density);
}
protected boolean isShowBanner()
{
// Do your checks here, like whether the user payed for ad removement.
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
if (!isShowBanner())
{
return;
}
int width = r - l;
if (width <= 0)
{
return;
}
AdSize adSize = AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(getContext(), getDpWidth(width));
// Prevent the ad from beeing added with each layout cicle,
// by checking, whether or not available size actually changed the format of the banner
if (mAdView == null || !adSize.equals(mAdView.getAdSize()))
{
removeAllViews();
mAdView = new AdView(getContext());
mAdView.setAdSize(adSize);
((GameApplication) getContext().getApplicationContext()).androidInjector().getAdService().loadBannerAd(getRootActivity(this), mAdView);
this.addView(mAdView);
}
mAdView.layout(0, 0, width, b - t);
}
}
Related
The previous version of my question was too wordy. People couldn't understand it, so the following is a complete rewrite. See the edit history if you are interested in the old version.
A RelativeLayout parent sends MeasureSpecs to its child view's onMeasure method in order to see how big the child would like to be. This occurs in several passes.
My custom view
I have a custom view. As the view's content increases, the view's height increases. When the view reaches the maximum height that the parent will allow, the view's width increases for any additional content (as long as wrap_content was selected for the width). Thus, the width of the custom view is directly dependant on what parent says the maximum hight must be.
An (inharmonious) parent child conversation
onMeasure pass 1
The RelativeLayout parent tells my view, "You can be any width up to 900 and any height up to 600. What do you say?"
My view says, "Well, at that height, I can fit everything with a width of 100. So I'll take a width of 100 and a height of 600."
onMeasure pass 2
The RelativeLayout parent tells my view, "You told me last time that you wanted a width of 100, so let's set that as an exact width. Now, based on that width, what kind of height would you like? Anything up to 500 is OK."
"Hey!" my view replies. "If you're only giving me a maximum hight of 500, then 100 is too narrow. I need a width of 200 for that height. But fine, have it your way. I won't break the rules (yet...). I'll take a width of 100 and a height of 500."
Final result
The RelativeLayout parent assigns the view a final size of 100 for the width and 500 for the height. This is of course too narrow for the view and part of the content gets clipped.
"Sigh," thinks my view. "Why won't my parent let me be wider? There is plenty of room. Maybe someone on Stack Overflow can give me some advice."
Update: Modified code to fix some things.
First, let me say that you asked a great question and laid out the problem very well (twice!) Here is my go at a solution:
It seems that there is a lot going on with onMeasure that, on the surface, doesn't make a lot of sense. Since that is the case, we will let onMeasure run as it will and at the end pass judgment on the View's bounds in onLayoutby setting mStickyWidth to the new minimum width we will accept. In onPreDraw, using a ViewTreeObserver.OnPreDrawListener, we will force another layout (requestLayout). From the documentation (emphasis added):
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.
The new minimum width set in onLayout will now be enforced by onMeasure which is now smarter about what is possible.
I have tested this with your example code and it seems to work OK. It will need much more testing. There may be other ways to do this, but that is the gist of the approach.
CustomView.java
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
public class CustomView extends View
implements ViewTreeObserver.OnPreDrawListener {
private int mStickyWidth = STICKY_WIDTH_UNDEFINED;
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
logMeasureSpecs(widthMeasureSpec, heightMeasureSpec);
int desiredHeight = 10000; // some value that is too high for the screen
int desiredWidth;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
// Height
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
// Width
if (mStickyWidth != STICKY_WIDTH_UNDEFINED) {
// This is the second time through layout and we are trying renogitiate a greater
// width (mStickyWidth) without breaking the contract with the View.
desiredWidth = mStickyWidth;
} else if (height > BREAK_HEIGHT) { // a number between onMeasure's two final height requirements
desiredWidth = ARBITRARY_WIDTH_LESSER; // arbitrary number
} else {
desiredWidth = ARBITRARY_WIDTH_GREATER; // arbitrary number
}
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
Log.d(TAG, "setMeasuredDimension(" + width + ", " + height + ")");
setMeasuredDimension(width, height);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int w = right - left;
int h = bottom - top;
super.onLayout(changed, left, top, right, bottom);
// Here we need to determine if the width has been unnecessarily constrained.
// We will try for a re-fit only once. If the sticky width is defined, we have
// already tried to re-fit once, so we are not going to have another go at it since it
// will (probably) have the same result.
if (h <= BREAK_HEIGHT && (w < ARBITRARY_WIDTH_GREATER)
&& (mStickyWidth == STICKY_WIDTH_UNDEFINED)) {
mStickyWidth = ARBITRARY_WIDTH_GREATER;
getViewTreeObserver().addOnPreDrawListener(this);
} else {
mStickyWidth = STICKY_WIDTH_UNDEFINED;
}
Log.d(TAG, ">>>>onLayout: w=" + w + " h=" + h + " mStickyWidth=" + mStickyWidth);
}
#Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
if (mStickyWidth == STICKY_WIDTH_UNDEFINED) { // Happy with the selected width.
return true;
}
Log.d(TAG, ">>>>onPreDraw() requesting new layout");
requestLayout();
return false;
}
protected void logMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
String measureSpecHeight;
String measureSpecWidth;
if (heightMode == MeasureSpec.EXACTLY) {
measureSpecHeight = "EXACTLY";
} else if (heightMode == MeasureSpec.AT_MOST) {
measureSpecHeight = "AT_MOST";
} else {
measureSpecHeight = "UNSPECIFIED";
}
if (widthMode == MeasureSpec.EXACTLY) {
measureSpecWidth = "EXACTLY";
} else if (widthMode == MeasureSpec.AT_MOST) {
measureSpecWidth = "AT_MOST";
} else {
measureSpecWidth = "UNSPECIFIED";
}
Log.d(TAG, "Width: " + measureSpecWidth + ", " + widthSize + " Height: "
+ measureSpecHeight + ", " + heightSize);
}
private static final String TAG = "CustomView";
private static final int STICKY_WIDTH_UNDEFINED = -1;
private static final int BREAK_HEIGHT = 1950;
private static final int ARBITRARY_WIDTH_LESSER = 200;
private static final int ARBITRARY_WIDTH_GREATER = 800;
}
To make custom layout you need to read and understand this article https://developer.android.com/guide/topics/ui/how-android-draws.html
It isn't difficult to implement behaviour you want. You just need to override onMeasure and onLayout in your custom view.
In onMeasure you will get max possible height of your custom view and call measure() for childs in cycle. After child measurement get desired height from each child and calculate is child fit in current column or not, if not increase custom view wide for new column.
In onLayout you must call layout() for all child views to set them positions within the parent. This positions you have calculated in onMeasure before.
If you have a TextView with layout_width="wrap_content" and it has to wrap to a second line to contain the text, then it will size its width to use up all of the space available (respecting margins etc). But why is there padding at the end of the view? I just told it to wrap_content, so it should wrap that content! This seems like a bug, this is visible in the chat UI of the stock Messenger app. (The image is from my own app though. But that extra space is definitely not in the 9 patch.)
Any workaround?
Update: Responders/commenters missed the point. Maybe the image I uploaded was misleading because it was styled from my app. The problem occurs with any TextView, you can see by styling the background that the view bounds will no longer be tight. I uploaded a different image. Here is the XML for the TextViews in the image:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:background="#dddddd"
android:text="This doesn't wrap"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:layout_gravity="center_horizontal"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginStart="20dp"
android:layout_gravity="center_horizontal"
android:background="#dddddd"
android:text="This wraps and look, the bounds does not fit tight against the right edge of text"
/>
I found the selected answer to be helpful, although it didn't quite account for padding. I combined the selected answer with this post's answer to come up with a view that works with padding. FYI, the other post's answer had a flaw where the view would sometimes get cut-off at the end by a few pixels.
public class TightTextView extends TextView {
public TightTextView(Context context) {
super(context);
}
public TightTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TightTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int specModeW = MeasureSpec.getMode(widthMeasureSpec);
if (specModeW != MeasureSpec.EXACTLY) {
Layout layout = getLayout();
if (layout != null) {
int w = (int) Math.ceil(getMaxLineWidth(layout)) + getCompoundPaddingLeft() + getCompoundPaddingRight();
if (w < getMeasuredWidth()) {
super.onMeasure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST),
heightMeasureSpec);
}
}
}
}
private float getMaxLineWidth(Layout layout) {
float max_width = 0.0f;
int lines = layout.getLineCount();
for (int i = 0; i < lines; i++) {
if (layout.getLineWidth(i) > max_width) {
max_width = layout.getLineWidth(i);
}
}
return max_width;
}
}
At first, when seeing your post, I thought that the problem was because standard Android TextView has some default padding defined in their base style. If one wants to remove it, one can try it something like:
android:paddingEnd="0dp"
or
android:paddingRight="0dp"
As your post has been updated, I understand that your problem does not come from padding, but from word wrapping. Indeed, when there are several lines to display, Android TextView use the whole available space in width.
As stated in this post, there is no standard solution for this and you will need to customize your text view to fix its width after filling it.
Overriding onMeasure method of your textView like below should work (inspired from "sky" answer) :
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int specModeW = MeasureSpec.getMode(widthMeasureSpec);
if (specModeW != MeasureSpec.EXACTLY) {
Layout layout = getLayout();
int linesCount = layout.getLineCount();
if (linesCount > 1) {
float textRealMaxWidth = 0;
for (int n = 0; n < linesCount; ++n) {
textRealMaxWidth = Math.max(textRealMaxWidth, layout.getLineWidth(n));
}
int w = (int) Math.ceil(textRealMaxWidth);
if (w < getMeasuredWidth()) {
super.onMeasure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST),
heightMeasureSpec);
}
}
}
}
I recently faced a similar problem when developing a chat-bubble view for an app, so I used the ideas from the accepted solution, and #hoffware's improvements, and re-implemented them in Kotlin.
import android.content.Context
import android.util.AttributeSet
import android.view.View.MeasureSpec.*
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil
class TightTextView
#JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : AppCompatTextView(context, attrs, defStyleAttr) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val lineCount = layout.lineCount
if (lineCount > 1 && getMode(widthMeasureSpec) != EXACTLY) {
val textWidth = (0 until lineCount).maxOf(layout::getLineWidth)
val padding = compoundPaddingLeft + compoundPaddingRight
val w = ceil(textWidth).toInt() + padding
if (w < measuredWidth) {
val newWidthMeasureSpec = makeMeasureSpec(w, AT_MOST)
super.onMeasure(newWidthMeasureSpec, heightMeasureSpec)
}
}
}
}
Pretty sure this is because "against" doesn't fit between the word "fit" and the right edge. If you want to test this out, try changing your text to a bunch of |'s with a single space between each one.
I have a GridView. The data of GridView is request from a server.
Here is the item layout in GridView:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/analysis_micon_bg"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="#dimen/half_activity_vertical_margin"
android:paddingLeft="#dimen/half_activity_horizontal_margin"
android:paddingRight="#dimen/half_activity_horizontal_margin"
android:paddingTop="#dimen/half_activity_vertical_margin" >
<ImageView
android:id="#+id/ranking_prod_pic"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="#string/app_name"
android:scaleType="centerCrop" />
<TextView
android:id="#+id/ranking_rank_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="#+id/ranking_prod_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="#+id/ranking_prod_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
I request data from server, get image url and load image to Bitmap
public static Bitmap loadBitmapFromInputStream(InputStream is) {
return BitmapFactory.decodeStream(is);
}
public static Bitmap loadBitmapFromHttpUrl(String url) {
try {
return loadBitmapFromInputStream((InputStream) (new URL(url).getContent()));
} catch (Exception e) {
Log.e(TAG, e.getMessage());
return null;
}
}
and there is the code of getView(int position, View convertView, ViewGroup parent) method in adapter
Bitmap bitmap = BitmapUtil.loadBitmapFromHttpUrl(product.getHttpUrl());
prodImg.setImageBitmap(bitmap);
The image size is 210*210. I run my application on my Nexus 4. The image does fill ImageView width, but the ImageView height does not scale. ImageView does not show the whole image.
How do I solve this problem?
Without using any custom classes or libraries:
<ImageView
android:id="#id/img"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:scaleType="fitCenter" />
scaleType="fitCenter" (default when omitted)
will make it as wide as the parent allows and up/down-scale as needed keeping aspect ratio.
scaleType="centerInside"
if the intrinsic width of src is smaller than parent widthwill center the image horizontally
if the intrinsic width of src is larger than parent widthwill make it as wide as the parent allows and down-scale keeping aspect ratio.
It doesn't matter if you use android:src or ImageView.setImage* methods and the key is probably the adjustViewBounds.
I like answer of arnefm but he made a small mistake (see comments) which I will try to correct:
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* ImageView that keeps aspect ratio when scaled
*/
public class ScaleImageView extends ImageView {
public ScaleImageView(Context context) {
super(context);
}
public ScaleImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScaleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
try {
Drawable drawable = getDrawable();
if (drawable == null) {
setMeasuredDimension(0, 0);
} else {
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
if (measuredHeight == 0 && measuredWidth == 0) { //Height and width set to wrap_content
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (measuredHeight == 0) { //Height set to wrap_content
int width = measuredWidth;
int height = width * drawable.getIntrinsicHeight() / drawable.getIntrinsicWidth();
setMeasuredDimension(width, height);
} else if (measuredWidth == 0){ //Width set to wrap_content
int height = measuredHeight;
int width = height * drawable.getIntrinsicWidth() / drawable.getIntrinsicHeight();
setMeasuredDimension(width, height);
} else { //Width and height are explicitly set (either to match_parent or to exact value)
setMeasuredDimension(measuredWidth, measuredHeight);
}
}
} catch (Exception e) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
Thus your ImageView will be scaled properly and will have no dimension problems if (for instance) put inside of ScrollView
I had a similar problem once. I solved it by making a custom ImageView.
public class CustomImageView extends ImageView
Then override the onMeasure method of the imageview. I did something like this I believe:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
try {
Drawable drawable = getDrawable();
if (drawable == null) {
setMeasuredDimension(0, 0);
} else {
float imageSideRatio = (float)drawable.getIntrinsicWidth() / (float)drawable.getIntrinsicHeight();
float viewSideRatio = (float)MeasureSpec.getSize(widthMeasureSpec) / (float)MeasureSpec.getSize(heightMeasureSpec);
if (imageSideRatio >= viewSideRatio) {
// Image is wider than the display (ratio)
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = (int)(width / imageSideRatio);
setMeasuredDimension(width, height);
} else {
// Image is taller than the display (ratio)
int height = MeasureSpec.getSize(heightMeasureSpec);
int width = (int)(height * imageSideRatio);
setMeasuredDimension(width, height);
}
}
} catch (Exception e) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
This will stretch the image to fit the screen while maintaining the aspect ratio.
Use android:scaleType="centerCrop".
FOR IMAGE VIEW (set these parameters)
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:scaleType = "fitCenter"
android:adjustViewBounds = "true"
Now whatever the size of the image is there, it's width will match the parent and height will be according to match the ratio. I have tested this and I am 100% sure.
we want this -->
not this -->
// Results will be:
Image width -> stretched as match parent
Image height -> according to image width (maximum to aspect ratio)
// like the first one
I did something similar to the above and then banged my head against the wall for a few hours because it did not work inside a RelativeLayout. I ended up with the following code:
package com.example;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
public class ScaledImageView extends ImageView {
public ScaledImageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final Drawable d = getDrawable();
if (d != null) {
int width;
int height;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
height = MeasureSpec.getSize(heightMeasureSpec);
width = (int) Math.ceil(height * (float) d.getIntrinsicWidth() / d.getIntrinsicHeight());
} else {
width = MeasureSpec.getSize(widthMeasureSpec);
height = (int) Math.ceil(width * (float) d.getIntrinsicHeight() / d.getIntrinsicWidth());
}
setMeasuredDimension(width, height);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
And then to prevent RelativeLayout from ignoring the measured dimension I did this:
<FrameLayout
android:id="#+id/image_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="#+id/something">
<com.example.ScaledImageView
android:id="#+id/image"
android:layout_width="wrap_content"
android:layout_height="150dp"/>
</FrameLayout>
This will not be applicable if you set image as background in ImageView, need to set at src(android:src).
Thanks.
Yo don't need any java code. You just have to :
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="centerCrop" />
The key is in the match parent for width and height
To create an image with width equals screen width, and height proportionally set according to aspect ratio, do the following.
Glide.with(context).load(url).asBitmap().into(new SimpleTarget<Bitmap>() {
#Override
public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
// creating the image that maintain aspect ratio with width of image is set to screenwidth.
int width = imageView.getMeasuredWidth();
int diw = resource.getWidth();
if (diw > 0) {
int height = 0;
height = width * resource.getHeight() / diw;
resource = Bitmap.createScaledBitmap(resource, width, height, false);
}
imageView.setImageBitmap(resource);
}
});
Hope this helps.
Use these properties in ImageView to keep aspect ratio:
android:adjustViewBounds="true"
android:scaleType="fitXY"
You can try to do what you're doing by manually loading the images, but I would very very strongly recommend taking a look at Universal Image Loader.
I recently integrated it into my project and I have to say its fantastic. Does all the worrying about making things asynchronous, resizing, caching images for you. It's really easy to integrate and set up. Within 5 minutes you can probably get it doing what you want.
Example code:
//ImageLoader config
DisplayImageOptions displayimageOptions = new DisplayImageOptions.Builder().showStubImage(R.drawable.downloadplaceholder).cacheInMemory().cacheOnDisc().showImageOnFail(R.drawable.loading).build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext()).
defaultDisplayImageOptions(displayimageOptions).memoryCache(new WeakMemoryCache()).discCache(new UnlimitedDiscCache(cacheDir)).build();
if (ImageLoader.getInstance().isInited()) {
ImageLoader.getInstance().destroy();
}
ImageLoader.getInstance().init(config);
imageLoadingListener = new ImageLoadingListener() {
#Override
public void onLoadingStarted(String s, View view) {
}
#Override
public void onLoadingFailed(String s, View view, FailReason failReason) {
ImageView imageView = (ImageView) view;
imageView.setImageResource(R.drawable.android);
Log.i("Failed to Load " + s, failReason.toString());
}
#Override
public void onLoadingComplete(String s, View view, Bitmap bitmap) {
}
#Override
public void onLoadingCancelled(String s, View view) {
}
};
//Imageloader usage
ImageView imageView = new ImageView(getApplicationContext());
if (orientation == 1) {
imageView.setLayoutParams(new LinearLayout.LayoutParams(width / 6, width / 6));
} else {
imageView.setLayoutParams(new LinearLayout.LayoutParams(height / 6, height / 6));
}
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageLoader.displayImage(SERVER_HOSTNAME + "demos" + demo.getPathRoot() + demo.getRootName() + ".png", imageView, imageLoadingListener);
This can lazy load the images, fit them correctly to the size of the imageView showing a placeholder image while it loads, and showing a default icon if loading fails and caching the resources.
-- I should also add that this current config keeps the image aspect ratio, hence applicable to your original question
Try this: it solved the problem for me
android:adjustViewBounds="true"
android:scaleType="fitXY"
Just use UniversalImageLoader and set
DisplayImageOptions.Builder()
.imageScaleType(ImageScaleType.EXACTLY_STRETCHED)
.build();
and no scale settings on ImageView
try with this simple line... add this line in your xml code in image view tag with out adding any dependency
android:scaleType="fitXY"
use android:ScaleType="fitXY" im ImageView xml
I had a similar issue, I found the reason for this is because you need to calculate the dp. Android studio is calculating the ImageView when you load it from the drawable, but when you are using another method, like loading from bitmap the dp is not automatically accounted for,
Here is my xml
<ImageView
android:id="#+id/imageViewer"
android:layout_width="match_parent"
android:layout_height="match_parent"//dp is not automaticly updated, when loading from a other source
android:scaleType="fitCenter"
tools:srcCompat="#drawable/a8" />
I'm using Kotlin, and loading drawable from an asset file, here's how I calculate this
val d = Drawable.createFromStream(assets.open("imageData/${imageName}.png"), null)
bitHeight = d.minimumHeight//get the image height
imageViewer.layoutParams.height = (bitHeight * resources.displayMetrics.density).toInt()//set the height
imageViewer.setImageDrawable(d)//set the image from the drawable
imageViewer.requestLayout()//here I apply it to the layout
In my app I have a time display which updates every second. Each time the TextView used for the seconds field changes, the Developer Options->Show surface updates tool flashes the entire screen. I've looked around and can really only find this question which pretty well clarifies that there is no way to prevent the TextView from causing a relayout for at least part of the window. So I was sure to verify that my TextView's are wrapped in their own container but I still have the same issue. Every call to setText() causes the entire view to flash.
My hierarchy is as follows:
Fragment
RelativeLayout (Fragment Root View)
LinearLayout
RelativeLayout
My Time TextViews
Various other view components which change rarely
I would like to fix this if possible. I do need to try and reduce my view count if possible and I plan on working on it but this is still a problem I would like to remove from the app.
Show surface updates flashes the entire screen when hardware acceleration is on but it does not mean the entire window was redrawn. There is another option you can use that shows you exactly what part of the screen was redrawn when hardware acceleration is on ("Show GPU view updates").
I encountered a similar problem where I was updating a timer which caused a measure pass every time. If you have something that is expensive to measure, like a ListView, it affects performance. In my case, I had a ListView that didn't need to change size when the timer updated, so I made a custom ListView holder to stop the the measure from cascading down to the ListView. Now it runs smoothly!
public class CustomListviewHolderLayout extends FrameLayout {
public int currentWidth = 0;
public int currentHeight = 0;
public CustomListviewHolderLayout(Context context) {
super(context);
}
public CustomListviewHolderLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomListviewHolderLayout(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
//Debug Prints
String wMode = "";
switch(MeasureSpec.getMode(widthMeasureSpec)){
case MeasureSpec.AT_MOST:
wMode = "AT_MOST"; break;
case MeasureSpec.EXACTLY:
wMode = "EXACTLY"; break;
case MeasureSpec.UNSPECIFIED:
wMode = "UNSPECIFIED"; break;
default:
wMode = "";
}
String hMode = "";
switch(MeasureSpec.getMode(heightMeasureSpec)){
case MeasureSpec.AT_MOST:
hMode = "AT_MOST"; break;
case MeasureSpec.EXACTLY:
hMode = "EXACTLY"; break;
case MeasureSpec.UNSPECIFIED:
hMode = "UNSPECIFIED"; break;
default:
hMode = "";
}
Log.i("RandomCustomListViewHolder", "wMode = " + wMode + ", size = " + MeasureSpec.getSize(widthMeasureSpec));
Log.i("RandomCustomListViewHolder", "hMode = " + hMode + ", size = " + MeasureSpec.getSize(heightMeasureSpec));
//only remeasure child listview when the size changes (e.g. orientation change)
if(getChildCount() == 1){
if(((MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) ||
(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST))
&&((MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) ||
(MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST))){
//check if height dimensions are different than before
int newWidth = MeasureSpec.getSize(widthMeasureSpec);
int newHeight = MeasureSpec.getSize(heightMeasureSpec);
if((newWidth != currentWidth) || (newHeight != currentHeight)){
//remeasure if different
Log.i("RandomCustomListViewHolder", "measuring listView");
View childView = getChildAt(0);
childView.measure(widthMeasureSpec, heightMeasureSpec);
currentWidth = newWidth;
currentHeight = newHeight;
this.setMeasuredDimension(newWidth, newHeight);
} else {
//still set this view's measured dimension
this.setMeasuredDimension(newWidth, newHeight);
}
} else {
//Specify match parent if one of the dimensions is unspecified
this.setMeasuredDimension(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
} else {
//view does not have the listview child, measure normally
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
currentWidth = 0;
currentHeight = 0;
}
}
}
I have a VideoView which is set up like this:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/player"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<VideoView
android:id="#+id/video"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_centerVertical="true" />
<ProgressBar
android:id="#+id/loader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true" />
</RelativeLayout>
But the VideoView matches the width of the parent container, and then the height is set according to the aspect ratio of the loaded movie.
I would like to do just the opposite, I want the VideoView to match the height of the parent while keeping the aspect ratio intact, the video will be clipped on the sides.
I managed to stretch the VideoView to fill the parent but then the aspect ratio is not kept.
Another thing is, I'm adding MediaController to the VideoView like this:
MediaController controllers = new MediaController(this) {
#Override
public void hide() {
if (state != State.Hidden) {
this.show();
}
else {
super.hide();
}
}
};
controllers.setAnchorView(videoView);
videoView.setMediaController(controllers);
videoView.setOnPreparedListener(new OnPreparedListener() {
#Override
public void onPrepared(MediaPlayer mediaPlayer) {
controllers.show();
}
});
This works great, and the controllers always stay on, but the height of the controllers is not being taken into account when calculating where to place the video (since it's vertically centered).
My two questions then are:
How do I make the VideoView match the height of the parent yet keep the aspect ratio?
How do I make the VideoView take into account the height of it's controllers?
Thanks.
You should extends from the built-in video view.
Call setVideoSize before video view is shown, you can get video size from thumbnail extracted from video.
So that, when video view's onMeasure is called, both mVideoWidth & mVideoHeight are > 0.
If you want to account the height of controllers, you can do it yourself in the onMeasure method.
Hope will help.
public class MyVideoView extends VideoView {
private int mVideoWidth;
private int mVideoHeight;
public MyVideoView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyVideoView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyVideoView(Context context) {
super(context);
}
public void setVideoSize(int width, int height) {
mVideoWidth = width;
mVideoHeight = height;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Log.i("###", "onMeasure");
int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
if (mVideoWidth > 0 && mVideoHeight > 0) {
if (mVideoWidth * height > width * mVideoHeight) {
// Log.i("###", "image too tall, correcting");
height = width * mVideoHeight / mVideoWidth;
} else if (mVideoWidth * height < width * mVideoHeight) {
// Log.i("###", "image too wide, correcting");
width = height * mVideoWidth / mVideoHeight;
} else {
// Log.i("###", "aspect ratio is correct: " +
// width+"/"+height+"="+
// mVideoWidth+"/"+mVideoHeight);
}
}
// Log.i("###", "setting size: " + width + 'x' + height);
setMeasuredDimension(width, height);
}
}
I solved this problem with layout. It seems that it worked fine when it was pinned to the corners but it caused the video to skew. To test I changed my relative layout's background to #990000 to see the red poking through.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/relative_parent"
android:background="#000000">
<VideoView
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:focusable="false"
android:focusableInTouchMode="false"
android:id="#+id/videoView" />
</RelativeLayout>
Regarding question 1, I am surprised no one has mentioned the possible use of the MediaPlayer's scaling mode.
https://developer.android.com/reference/android/media/MediaPlayer.html#setVideoScalingMode(int)
It has 2 modes. Both of them always fill the view area. To get it to fill the space while preserving the aspect ratio, thus cropping the long side, you need to switch to the second mode, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING. That solves one part of the problem. The other part is to change VideoView's measuring behavior, just as some of the other answers demonstrate. This is the way I did it, mostly out of laziness and not familiar with the metadata API's that the others use, you are welcome to use this method or one of the other methods to fix the size of the view. The blanket catch ensures safety when this is called before mMediaPlayer exists, as it may be called many times, and also falls back to old behavior should the field name ever change.
class FixedSizeVideoView : VideoView {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
// rather than shrink down to fit, stay at the size requested by layout params. Let the scaling mode
// of the media player shine through. If the scaling mode on the media player is set to the one
// with cropping, you can make a player similar to AVLayerVideoGravityResizeAspectFill on iOS
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
try {
val mpField = VideoView::class.java.getDeclaredField("mMediaPlayer")
mpField.isAccessible = true
val mediaPlayer: MediaPlayer = mpField.get(this) as MediaPlayer
val width = View.getDefaultSize(mediaPlayer.videoWidth, widthMeasureSpec)
val height = View.getDefaultSize(mediaPlayer.videoHeight, heightMeasureSpec)
setMeasuredDimension(width, height)
}
catch (ex: Exception) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
}
So using this class in the layout, you just change the scaling mode on the media Player wherever you have a chance. Such as:
video.setOnPreparedListener { mp: MediaPlayer ->
mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING)
mp.isLooping = true
mp.setScreenOnWhilePlaying(false)
}
video.start()
public class MyVideoView extends VideoView {
private int mVideoWidth;
private int mVideoHeight;
public MyVideoView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyVideoView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyVideoView(Context context) {
super(context);
}
#Override
public void setVideoURI(Uri uri) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(this.getContext(), uri);
mVideoWidth = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
mVideoHeight = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
super.setVideoURI(uri);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Log.i("###", "onMeasure");
int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
if (mVideoWidth > 0 && mVideoHeight > 0) {
if (mVideoWidth * height > width * mVideoHeight) {
// Log.i("###", "image too tall, correcting");
height = width * mVideoHeight / mVideoWidth;
} else if (mVideoWidth * height < width * mVideoHeight) {
// Log.i("###", "image too wide, correcting");
width = height * mVideoWidth / mVideoHeight;
} else {
// Log.i("###", "aspect ratio is correct: " +
// width+"/"+height+"="+
// mVideoWidth+"/"+mVideoHeight);
}
}
// Log.i("###", "setting size: " + width + 'x' + height);
setMeasuredDimension(width, height);
}
}
Quick and efficient fix:
No need to create a custom view extending from VideoView. Just set a value big enough to android:layout_width. This will set the widthSpecMode of the video view to View.MeasureSpec.AT_MOST and then the onMeasure() method of VideoView will auto-adjust its width keeping the ratio.
<VideoView
android:id="#+id/video"
android:layout_width="2000dp"
android:layout_height="match_parent" />
Using ConstraintLayout we can achieve this, refer below xml code.
When layout_width and layout_height are 0dp, the size and position of the VideoView are calculated dynamically based on the other constraints. The layout_constraintDimensionRatio attribute indicates that when the app calculates the size of the VideoView, the ratio of the width to the height should be 3:4. This constraint keeps the aspect ratio of the video the same and prevents the view from being stretched too far in either direction (depending on how the device is rotated).
Change layout_constraintDimensionRatio value depending on requirement Portrait/Landscape.
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<VideoView
android:id="#+id/videoView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
For the first time a question answered my issue instead of answers!!
My issue was that I had a white space under the video on full screen. I was setting the layout_height to match_parent. The solution was to set it to wrap_content and give the parent a black background. That, and having the VideoView centered vertically in its parent.
I wrote this as a comment but then thought someone might have the same
issue I had, so here it is as an answer also.
I've tried a lot of solutions, while my video was always in 1000*1000 format, so I've created an easy solution for people who know their aspect ratio. First create a VideoView in a RelativeLayout like this:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/video_holder"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:clipToPadding="false">
<VideoView
android:id="#+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
</RelativeLayout>
Then before you load the video change the height and with programmatically like this:
int i = videoView.getHeight() > videoView.getWidth() ? videoView.getHeight() : videoView.getWidth();
video_holder.setLayoutParams(new ConstraintLayout.LayoutParams(i, i));
Of course this only works with 1:1 aspect ratio's but you could just use your aspect ratio to change either the height or the width.
Jobbert's answer in Kotlin, in case anyone needs it:
val max = if (videoView.height > videoView.width) videoView.height else videoView.width
videoView.layoutParams = ConstraintLayout.LayoutParams(max, max)
I have been looking for ways to display video in aspect fill in VideoView but after trying many solutions, none of them seems to work.
So I implemented the following approach and it's working for me:
Code:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// getting screen size
((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int width = displayMetrics.widthPixels;
int height = displayMetrics.heightPixels;
double videoSizeRatio = (double) mVideoHeight / mVideoWidth;
double screenSizeRatio = (double) height / width;
if (mVideoWidth > 0 && mVideoHeight > 0) {
if (videoSizeRatio > screenSizeRatio) { // screen is wider than video width
height = (int) (videoSizeRatio * width);
} else if (videoSizeRatio < screenSizeRatio) {
width = (int) (height / videoSizeRatio);
}
}
setMeasuredDimension(width, height);
}
Layout:
<YourCustomizedVideoView
android:id="#+id/videoView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
The best way to do so is
Set width of videoview to max
I was facing the same issue.
I just set the Width to 999px
and Height to match parent
It works.
We get a perfect view like Tik tok or Instagram reels
Just put your VideoView inside the RelativeLayout and set the desired size for that relative layout. like below code,
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="300dp">
<VideoView
android:id="#+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
</RelativeLayout>
It will work.
You just need to put Videoview widget in RelativeLayout (changes in xml file only)
here is reference answer link