I'm coding android and I have a imageview. I want to set scaletype of this to topcrop.
I could find centercrop in options, but it's not my request. How do I do?
Custom Android ImageView for top-crop scaling of the contained drawable.
import android.content.Context;
import android.graphics.Matrix;
import android.widget.ImageView;
/**
* ImageView to display top-crop scale of an image view.
*
* #author Chris Arriola
*/
public class TopCropImageView extends ImageView {
public TopCropImageView(Context context) {
super(context);
setScaleType(ScaleType.MATRIX);
}
#Override
protected boolean setFrame(int l, int t, int r, int b) {
final Matrix matrix = getImageMatrix();
float scale;
final int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
final int drawableWidth = getDrawable().getIntrinsicWidth();
final int drawableHeight = getDrawable().getIntrinsicHeight();
if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
scale = (float) viewHeight / (float) drawableHeight;
} else {
scale = (float) viewWidth / (float) drawableWidth;
}
matrix.setScale(scale, scale);
setImageMatrix(matrix);
return super.setFrame(l, t, r, b);
}
}
https://gist.github.com/arriolac/3843346
I just wanted a temporary solution without excess work . Try this
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:scrollY="-100dp"
android:id="#+id/poster"
/>
scrollY pulls your image up or down. Try it with different dp's and you 'll find the right size.
I don't believe you need a custom Image View for this. All you have to do is set the scaleType to matrix and set a custom matrix to the ImageView.
Refer: https://stackoverflow.com/a/38049348/4747587
Fixed version:
setFrame calling asap
not reusing Matrix -> it caused issue that when image realoaded too soon (with Picasso), the image was scaled too much
import android.content.Context
import android.graphics.Matrix
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
class TopCropImageView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AppCompatImageView(context, attrs, defStyleAttr) {
init {
scaleType = ScaleType.MATRIX
}
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
super.setFrame(l, t, r, b)
val matrix = Matrix()
val scale: Float
val viewWidth: Int = width - paddingLeft - paddingRight
val viewHeight: Int = height - paddingTop - paddingBottom
val drawableWidth: Int = drawable.intrinsicWidth
val drawableHeight: Int = drawable.intrinsicHeight
scale = if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
viewHeight.toFloat() / drawableHeight.toFloat()
} else {
viewWidth.toFloat() / drawableWidth.toFloat()
}
matrix.setScale(scale, scale)
imageMatrix = matrix
return true
}
}
Related
I'm building an Android application and am now designing the user profile activity.
It is supposed to display the profile picture at the top in a rectangle container that looks like a banner.
<ImageView
android:id="#+id/user_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:background="#android:color/transparent"
android:layout_centerHorizontal="true"
android:elevation="2dp"/>
This XML code works well if the image is a 1x1 ratio or if the height is bigger than the width. (I display a blurry and stretched version of the same image in background so it's not completely ugly).
The problem I hit is when the WIDTH is bigger than the HEIGHT. It tries to scale the height (in my case to 300dp) but then the width hits the borders of the layout and it stops resizing, leaving me with an empty space under the image.
I know that the scaleType="centerCrop" does pretty much what I'm looking for, but if the ratio of the image is 1x1 it's cropping the top part of the image (most of the time the hairs) of the person, which I don't want.
So basically, is there a way in XML, or in JAVA, to resize the height of an image to fill my container while keeping the width ratio and ignoring the max width of the parent layout ? (I imagine it's supposed to crop what would overflow just like "centerCrop" would).
Here is an image of what I'm currently acheiving and the problem occuring. The grey part is a blurry and resized version of the same Image, so it doesn't look too ugly when the image can't fill the width (for example, in a 1x1 ratio picture).
https://i.stack.imgur.com/FUDcP.png
I am not getting you properly, but you want keep the aspect ratio of Image, you should match_parent to android:height and try to wrap_content the width of ImageView. Try to fitCenter as android:scaleType.
Try the code bellow -
<ImageView
android:id="#+id/user_pic"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:adjustViewBounds="true"
android:background="#android:color/transparent"/>
The answer was found at this link.
Basically, the trick used is to make a custom ImageView class in Java, then use it. It will calculate the correct area to crop, if necessary, depending of the situation.
If anyone finds a pure XML solution, feel free to tell me !
Without any further wait, here is the code I used.
JAVA CLASS
public class FitYCropXImageView extends ImageView {
boolean done = false;
#SuppressWarnings("UnusedDeclaration")
public FitYCropXImageView(Context context) {
super(context);
setScaleType(ScaleType.MATRIX);
}
#SuppressWarnings("UnusedDeclaration")
public FitYCropXImageView(Context context, AttributeSet attrs) {
super(context, attrs);
setScaleType(ScaleType.MATRIX);
}
#SuppressWarnings("UnusedDeclaration")
public FitYCropXImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setScaleType(ScaleType.MATRIX);
}
private final RectF drawableRect = new RectF(0, 0, 0,0);
private final RectF viewRect = new RectF(0, 0, 0,0);
private final Matrix m = new Matrix();
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (done) {
return;//Already fixed drawable scale
}
final Drawable d = getDrawable();
if (d == null) {
return;//No drawable to correct for
}
int viewHeight = getMeasuredHeight();
int viewWidth = getMeasuredWidth();
int drawableWidth = d.getIntrinsicWidth();
int drawableHeight = d.getIntrinsicHeight();
drawableRect.set(0, 0, drawableWidth, drawableHeight);//Represents the original image
//Compute the left and right bounds for the scaled image
float viewHalfWidth = viewWidth / 2;
float scale = (float) viewHeight / (float) drawableHeight;
float scaledWidth = drawableWidth * scale;
float scaledHalfWidth = scaledWidth / 2;
viewRect.set(viewHalfWidth - scaledHalfWidth, 0, viewHalfWidth + scaledHalfWidth, viewHeight);
m.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.CENTER /* This constant doesn't matter? */);
setImageMatrix(m);
done = true;
requestLayout();
}
}
XML
<com.app.YourAppName.YourAppName.CustomViews.FitYCropXImageView
android:id="#+id/user_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"
android:elevation="2dp"
/>
JAVA ACTIVITY IF NECESSARY
import com.app.YourAppName.YourAppName.CustomViews.FitYCropXImageView;
FitYCropXImageView profilePic = (FitYCropXImageView) findViewById(R.id.user_pic);
If your ImageView width is match_parent and Your Original Image size is 300x400
and also assume that Deice's WIDTH_PIXEL 1080 and HEIGHT_PIXEL 1920
Your ImageView need to be 1080x1440(3:4)
You can use like below.
public static final int WIDTH_PIXEL = Resources.getSystem().getDisplayMetrics().widthPixels;
public static final int HEIGHT_PIXEL = Resources.getSystem().getDisplayMetrics().heightPixels;
//ratioX : ratioY = givenX : X
public static int getRatio(int ratioX, int ratioY, int givenX) {
int result = (int) ((((float) ratioY * (float) givenX) / (float) ratioX) + 0.5f);
//Log.v(TAG, "Request -> " + ratioX + " : " + ratioY + " = " + givenX + " : " + result);
return result;
}
{//Usage
int width = 300;
int height = 400;
int resultHeight = getRatio(width, height , WIDTH_PIXEL);
myImageVIew.getLayoutParams().height = resultHeight ;
}
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
I hope it can be a hint for you.
I'm attempting to extend ImageView and add a shadow. I'm running into a problem where the shadow is being clipped by the view bounds and looks quite bad:
I've attempted to set the width/height via LayoutParams programmatically as well as trying different XML properties like android:adjustViewBounds, but there is no change in the display. Similarly, setting a android:layout_margin is ineffective in preventing the shadow from being clipped.
Can anyone help me figure out how to avoid this clipping? I'm sure there is something obvious I'm overlooking here.
Update:
My code is very specific at this time to exactly one case: I'm trying to draw a circle "shadow" underneath a circle bitmap. It is obvious that the view bounds are causing the clipping, but I have not been able to find a solution that will allow me to expand the view bounds.
It has been claimed on #android-dev that my math is simply wrong. I am accounting for screen density, which is a common problem. I have triple checked my math on all counts and cannot find where it might be wrong.
Initially, on an xxhdpi screen, density 3.0, the 56dp image is exactly 168px wide and 168px high. After adding 2dp to the width and height to account for the offset, the layoutParams have width=174 and height=174.
My basic approach is to let the super ImageView do its thing and draw the bitmap specified in xml and all I want to do is draw a little something additionally. Is this approach fundamentally flawed?
I use the largest of width or height in onLayout to determine what the radius of my shadow circle should be: radius = Max(width, height) / 2. I draw a circle with this radius and center point at (Cx, Cy) where Cx is the midpoint of the width plus a x-offset and Cy is the midpoint of the height plus a y-offset to create the shadow effect. I draw the additional circle using a canvas to a bitmap and later in onDraw I place my circle on the canvas before allowing the ImageView super onDraw to take care of the source bitmap.
Additionally, in my onLayout I attempt to account for the x- and y-offset distances and add those to my view's width and height via LayoutParams, but no change in the size of the view is evidenced when the view is drawn.
Here is the code that I'm using: https://gitlab.com/dm78/ImageViewWithShadowExample/
Here is the relevant code:
activity_main.xml
<FrameLayout 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"
tools:context=".MainActivity">
<dm78.example.imageviewwithshadowexample.CustomShadowImageView
android:id="#+id/circle"
android:layout_gravity="center"
android:src="#drawable/circle"
android:layout_margin="16dp"
android:layout_width="56dp"
android:layout_height="56dp"/>
</FrameLayout>
CustomShadowImageView.java
package dm78.example.imageviewwithshadowexample;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.widget.FrameLayout;
import android.widget.ImageView;
public class CustomShadowImageView extends ImageView {
public static final String TAG = CustomShadowImageView.class.getSimpleName();
public static final float SHADOW_RADIUS_DP = 3f;
public static final float SHADOW_X_OFFSET_DP = 2f;
public static final float SHADOW_Y_OFFSET_DP = 2f;
private Paint mPaint;
private float mShadowRadius;
private float radius;
private float cx;
private float cy;
private float mShadowXOffset;
private float mShadowYOffset;
private Bitmap mShadowBitmap;
private FrameLayout.LayoutParams layoutParams;
private boolean expanded;
public CustomShadowImageView(Context context) {
super(context);
init();
}
public CustomShadowImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomShadowImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
Log.d(TAG, "init " + this.hashCode());
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
mShadowRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_RADIUS_DP, dm);
mShadowXOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_X_OFFSET_DP, dm);
mShadowYOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, SHADOW_Y_OFFSET_DP, dm);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//noinspection deprecation
int shadowColor = getContext().getResources().getColor(R.color.shadow);
mPaint.setColor(shadowColor);
expanded = false;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d(TAG, String.format("onMeasure %d w: %d, h: %d", this.hashCode(), MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.d(TAG, String.format("onLayout %d changed: %b, l: %d, t: %d, r: %d, b: %d", this.hashCode(), changed, left, top, right, bottom));
super.onLayout(changed, left, top, right, bottom);
if (changed) {
if (!expanded) {
layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
layoutParams.width = (int) (layoutParams.width + mShadowXOffset);
layoutParams.height = (int) (layoutParams.height + mShadowYOffset);
expanded = true;
}
cx = (right - left) / 2 + mShadowXOffset;
cy = (bottom - top) / 2 + mShadowYOffset;
boolean widthGreater = (right - left) > (bottom - top);
radius = (widthGreater ? right - left : bottom - top) / 2;
if (mShadowBitmap == null) {
Bitmap bitmap = Bitmap.createBitmap(right - left, bottom - top, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawCircle(cx, cy, radius, mPaint);
if (Build.VERSION.SDK_INT >= 17 && !isInEditMode()) {
RenderScript rs = RenderScript.create(getContext());
Allocation input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
Allocation output = Allocation.createTyped(rs, input.getType());
ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setRadius(mShadowRadius);
script.setInput(input);
script.forEach(output);
output.copyTo(bitmap);
}
mShadowBitmap = bitmap;
}
}
}
#Override
protected void onDraw(Canvas canvas) {
Log.d(TAG, "onDraw " + this.hashCode());
canvas.drawBitmap(mShadowBitmap, mShadowXOffset, mShadowYOffset, null);
super.onDraw(canvas);
}
}
By default, Views are only allowed by their parent to draw within their bounds and not beyond.
You have two options:
Either you add some padding to your Imageview to enlarge its bounds
instead of using layout_margin and you draw within these bounds.
Either you disable the child clipping behavior by setting android:clipChildren="false" to your FrameLayout.
If the cause is the view parent's padding, add the following line to the parent xml element:
android:clipToPadding="false"
android:clipChildren="false" in the parent layout worked for me
I'm trying to position an ImageView so that the bottom of the image is always pinned to the bottom of the view, no matter how small the height of the ImageView is. However, none of the scale types seem to fit what I am trying to do. CenterCrop is close, but I don't want the image to be centered. Similar to how CSS would handle absolute positioning.
The reason is, I need to animate the height of the ImageView, but make it seem as though it is "revealing" the upper portion of the image. I assume figuring out this method of cropping the image and animating the ImageView height is the easiest way to do this, but if someone knows of a better way I'd love being pointed in the right direction.
Any help appreciated.
Jpoliachik's answer was cool enough to make me wanna generalize it to support both top/bottom and left/right, by a variable amount. :) Now to top crop, just call setCropOffset(0,0) , bottom crop setCropOffset(0,1), left crop is also setCropOffset(0,0), and right crop setCropOffset(1,0). If you want to offset the viewport by some fraction of the image in one dimension, you can call e.g. setCropOffset(0, 0.25f) to shift it down by 25% of the non-viewable space, while 0.5f would center it. Cheers!
/**
* {#link android.widget.ImageView} that supports directional cropping in both vertical and
* horizontal directions instead of being restricted to center-crop. Automatically sets {#link
* android.widget.ImageView.ScaleType} to MATRIX and defaults to center-crop.
*/
public class CropImageView extends android.support.v7.widget.AppCompatImageView {
private static final float DEFAULT_HORIZONTAL_OFFSET = 0.5f;
private static final float DEFAULT_VERTICAL_OFFSET = 0.5f;
private float mHorizontalOffsetPercent = DEFAULT_HORIZONTAL_OFFSET;
private float mVerticalOffsetPercent = DEFAULT_VERTICAL_OFFSET;
public CropImageView(Context context) {
this(context, null);
}
public CropImageView(Context context, #Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CropImageView(Context context, #Nullable AttributeSet attrs, #AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
setScaleType(ScaleType.MATRIX);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
applyCropOffset();
}
/**
* Sets the crop box offset by the specified percentage values. For example, a center-crop would
* be (0.5, 0.5), a top-left crop would be (0, 0), and a bottom-center crop would be (0.5, 1)
*/
public void setCropOffset(float horizontalOffsetPercent, float verticalOffsetPercent) {
if (mHorizontalOffsetPercent < 0
|| mVerticalOffsetPercent < 0
|| mHorizontalOffsetPercent > 1
|| mVerticalOffsetPercent > 1) {
throw new IllegalArgumentException("Offset values must be a float between 0.0 and 1.0");
}
mHorizontalOffsetPercent = horizontalOffsetPercent;
mVerticalOffsetPercent = verticalOffsetPercent;
applyCropOffset();
}
private void applyCropOffset() {
Matrix matrix = getImageMatrix();
float scale;
int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
int drawableWidth = 0, drawableHeight = 0;
// Allow for setting the drawable later in code by guarding ourselves here.
if (getDrawable() != null) {
drawableWidth = getDrawable().getIntrinsicWidth();
drawableHeight = getDrawable().getIntrinsicHeight();
}
// Get the scale.
if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
// Drawable is flatter than view. Scale it to fill the view height.
// A Top/Bottom crop here should be identical in this case.
scale = (float) viewHeight / (float) drawableHeight;
} else {
// Drawable is taller than view. Scale it to fill the view width.
// Left/Right crop here should be identical in this case.
scale = (float) viewWidth / (float) drawableWidth;
}
float viewToDrawableWidth = viewWidth / scale;
float viewToDrawableHeight = viewHeight / scale;
float xOffset = mHorizontalOffsetPercent * (drawableWidth - viewToDrawableWidth);
float yOffset = mVerticalOffsetPercent * (drawableHeight - viewToDrawableHeight);
// Define the rect from which to take the image portion.
RectF drawableRect =
new RectF(
xOffset,
yOffset,
xOffset + viewToDrawableWidth,
yOffset + viewToDrawableHeight);
RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL);
setImageMatrix(matrix);
}
}
I ended up subclassing ImageView and creating a way to enable a 'BottomCrop' type image scaling.
I assigned the image to a RectF of the correct size by calculating the scale and expected image height based on the view height.
public class BottomCropImage extends ImageView {
public BottomCropImage(Context context) {
super(context);
setup();
}
public BottomCropImage(Context context, AttributeSet attrs) {
super(context, attrs);
setup();
}
public BottomCropImage(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
setup();
}
private void setup() {
setScaleType(ScaleType.MATRIX);
}
#Override
protected boolean setFrame(int l, int t, int r, int b) {
Matrix matrix = getImageMatrix();
float scale;
int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
int drawableWidth = getDrawable().getIntrinsicWidth();
int drawableHeight = getDrawable().getIntrinsicHeight();
//Get the scale
if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
scale = (float) viewHeight / (float) drawableHeight;
} else {
scale = (float) viewWidth / (float) drawableWidth;
}
//Define the rect to take image portion from
RectF drawableRect = new RectF(0, drawableHeight - (viewHeight / scale), drawableWidth, drawableHeight);
RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL);
setImageMatrix(matrix);
return super.setFrame(l, t, r, b);
}
}
I used #Jpoliachik code and it worked good, I made a couple of tweaks because sometimes getWidth and getHeight were returning 0 - getMeasuredWidth and getMeasuredHeight solved the problem.
#Override
protected boolean setFrame(int l, int t, int r, int b) {
if (getDrawable() == null)
return super.setFrame(l, t, r, b);
Matrix matrix = getImageMatrix();
float scale;
int viewWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
int viewHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
int drawableWidth = getDrawable().getIntrinsicWidth();
int drawableHeight = getDrawable().getIntrinsicHeight();
//Get the scale
if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
scale = (float) viewHeight / (float) drawableHeight;
} else {
scale = (float) viewWidth / (float) drawableWidth;
}
//Define the rect to take image portion from
RectF drawableRect = new RectF(0, drawableHeight - (viewHeight / scale), drawableWidth, drawableHeight);
RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL);
setImageMatrix(matrix);
return super.setFrame(l, t, r, b);
}
Based on qix's answer I've made a few improvements:
Created custom XML attributes. You don't have to call setCropOffset(). Instead you can just add app:verticalCropOffset and app:horizontalCropOffset to your XML layout (accepts both fractions and floats).
Addedapp:offsetScaleType attribute to control how the image is scaled:
crop: the same behavior as in the original answer, i. e. the image is scaled so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view; app:horizontalCropOffset and app:verticalCropOffset are then applied
fitInside: image is scaled so that both dimensions of the image will be equal to or less than the corresponding dimension of the view; app:horizontalFitOffset and app:verticalFitOffset are then applied
fitX: image is scaled so that its X dimension is equal to the view's X dimension. Y dimension is scaled so that the ratio is preserved. If image's Y dimension is larger than view's dimension, app:verticalCropOffset is applied, otherwise app:verticalFitOffset is applied
fitY: image is scaled so that its Y dimension is equal to the view's Y dimension. X dimension is scaled so that the ratio is preserved. If image's X dimension is larger than view's dimension, app:horizontalCropOffset is applied, otherwise app:horizontalFitOffset is applied
Converted code to Kotlin
Few minor refactorings for better Kotlin readability
We have to add a new OffsetImageView styleable to our attrs.xml:
<declare-styleable name="OffsetImageView">
<attr name="horizontalFitOffset" format="float|fraction" />
<attr name="verticalFitOffset" format="float|fraction" />
<attr name="horizontalCropOffset" format="float|fraction" />
<attr name="verticalCropOffset" format="float|fraction" />
<attr name="offsetScaleType" format="enum">
<enum name="crop" value="0"/>
<enum name="fitInside" value="1"/>
<enum name="fitX" value="2"/>
<enum name="fitY" value="3"/>
</attr>
</declare-styleable>
OffsetImageView code (add your own package and import your module's R file):
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Matrix
import android.graphics.RectF
import android.util.AttributeSet
import androidx.annotation.AttrRes
import androidx.annotation.StyleableRes
import androidx.appcompat.widget.AppCompatImageView
/**
* [android.widget.ImageView] that supports directional cropping in both vertical and
* horizontal directions instead of being restricted to center-crop. Automatically sets [ ] to MATRIX and defaults to center-crop.
*
* XML attributes (for offsets either a float or a fraction is allowed in values, e. g. 50% or 0.5):
* - app:verticalCropOffset
* - app:horizontalCropOffset
* - app:verticalFitOffset
* - app:horizontalFitOffset
* - app:offsetScaleType
*
* The `app:offsetScaleType` accepts one of the enum values:
* - crop: the same behavior as in the original answer, i. e. the image is scaled so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view; `app:horizontalCropOffset` and `app:verticalCropOffset` are then applied
* - fitInside: image is scaled so that both dimensions of the image will be equal to or less than the corresponding dimension of the view; `app:horizontalFitOffset` and `app:verticalFitOffset` are then applied
* - fitX: image is scaled so that its X dimension is equal to the view's X dimension. Y dimension is scaled so that the ratio is preserved. If image's Y dimension is larger than view's dimension, `app:verticalCropOffset` is applied, otherwise `app:verticalFitOffset` is applied
* - fitY: image is scaled so that its Y dimension is equal to the view's Y dimension. X dimension is scaled so that the ratio is preserved. If image's X dimension is larger than view's dimension, `app:horizontalCropOffset` is applied, otherwise `app:horizontalFitOffset` is applied
*/
class OffsetImageView(context: Context, attrs: AttributeSet?, #AttrRes defStyleAttr: Int) : AppCompatImageView(context, attrs, defStyleAttr) {
companion object {
private const val DEFAULT_HORIZONTAL_OFFSET = 0.5f
private const val DEFAULT_VERTICAL_OFFSET = 0.5f
}
enum class OffsetScaleType(val code: Int) {
CROP(0), FIT_INSIDE(1), FIT_X(2), FIT_Y(3)
}
private var mHorizontalCropOffsetPercent = DEFAULT_HORIZONTAL_OFFSET
private var mHorizontalFitOffsetPercent = DEFAULT_HORIZONTAL_OFFSET
private var mVerticalCropOffsetPercent = DEFAULT_VERTICAL_OFFSET
private var mVerticalFitOffsetPercent = DEFAULT_VERTICAL_OFFSET
private var mOffsetScaleType = OffsetScaleType.CROP
init {
scaleType = ScaleType.MATRIX
if (attrs != null) {
val a = context.obtainStyledAttributes(attrs, R.styleable.OffsetImageView, defStyleAttr, 0)
readAttrFloatValueIfSet(a, R.styleable.OffsetImageView_verticalCropOffset)?.let {
mVerticalCropOffsetPercent = it
}
readAttrFloatValueIfSet(a, R.styleable.OffsetImageView_horizontalCropOffset)?.let {
mHorizontalCropOffsetPercent = it
}
readAttrFloatValueIfSet(a, R.styleable.OffsetImageView_verticalFitOffset)?.let {
mVerticalFitOffsetPercent = it
}
readAttrFloatValueIfSet(a, R.styleable.OffsetImageView_horizontalFitOffset)?.let {
mHorizontalFitOffsetPercent = it
}
with (a) {
if (hasValue(R.styleable.OffsetImageView_offsetScaleType)) {
val code = getInt(R.styleable.OffsetImageView_offsetScaleType, -1)
if (code != -1) {
OffsetScaleType.values().find {
it.code == code
}?.let {
mOffsetScaleType = it
}
}
}
}
a.recycle()
}
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
applyOffset()
}
private fun readAttrFloatValueIfSet(typedArray: TypedArray, #StyleableRes index: Int): Float? {
try {
with(typedArray) {
if (!hasValue(index)) return null
var value = getFloat(index, -1f)
if (value >= 0) return value
value = getFraction(index, 1, 1, -1f)
if (value >= 0) return value
return null
}
} catch (e: RuntimeException) {
e.printStackTrace()
return null
}
}
/**
* Sets the crop box offset by the specified percentage values. For example, a center-crop would
* be (0.5, 0.5), a top-left crop would be (0, 0), and a bottom-center crop would be (0.5, 1)
*/
fun setOffsets(horizontalCropOffsetPercent: Float,
verticalCropOffsetPercent: Float,
horizontalFitOffsetPercent: Float,
verticalFitOffsetPercent: Float,
scaleType: OffsetScaleType) {
require(!(mHorizontalCropOffsetPercent < 0
|| mVerticalCropOffsetPercent < 0
|| mHorizontalFitOffsetPercent < 0
|| mVerticalFitOffsetPercent < 0
|| mHorizontalCropOffsetPercent > 1
|| mVerticalCropOffsetPercent > 1
|| mHorizontalFitOffsetPercent > 1
|| mVerticalFitOffsetPercent > 1)) { "Offset values must be a float between 0.0 and 1.0" }
mHorizontalCropOffsetPercent = horizontalCropOffsetPercent
mVerticalCropOffsetPercent = verticalCropOffsetPercent
mHorizontalFitOffsetPercent = horizontalFitOffsetPercent
mVerticalFitOffsetPercent = verticalFitOffsetPercent
mOffsetScaleType = scaleType
applyOffset()
}
private fun applyOffset() {
val matrix: Matrix = imageMatrix
val scale: Float
val viewWidth: Int = width - paddingLeft - paddingRight
val viewHeight: Int = height - paddingTop - paddingBottom
val drawable = drawable
val drawableWidth: Int
val drawableHeight: Int
if (drawable == null) {
drawableWidth = 0
drawableHeight = 0
} else {
// Allow for setting the drawable later in code by guarding ourselves here.
drawableWidth = drawable.intrinsicWidth
drawableHeight = drawable.intrinsicHeight
}
val scaleHeight = when (mOffsetScaleType) {
OffsetScaleType.CROP -> drawableWidth * viewHeight > drawableHeight * viewWidth // If drawable is flatter than view, scale it to fill the view height.
OffsetScaleType.FIT_INSIDE -> drawableWidth * viewHeight < drawableHeight * viewWidth // If drawable is is taller than view, scale according to height to fit inside.
OffsetScaleType.FIT_X -> false // User wants to fit X axis -> scale according to width
OffsetScaleType.FIT_Y -> true // User wants to fit Y axis -> scale according to height
}
// Get the scale.
scale = if (scaleHeight) {
viewHeight.toFloat() / drawableHeight.toFloat()
} else {
viewWidth.toFloat() / drawableWidth.toFloat()
}
val viewToDrawableWidth = viewWidth / scale
val viewToDrawableHeight = viewHeight / scale
if (drawableWidth >= viewToDrawableWidth && drawableHeight >= viewToDrawableHeight) {
val xOffset = mHorizontalCropOffsetPercent * (drawableWidth - viewToDrawableWidth)
val yOffset = mVerticalCropOffsetPercent * (drawableHeight - viewToDrawableHeight)
// Define the rect from which to take the image portion.
val drawableRect = RectF(
xOffset,
yOffset,
xOffset + viewToDrawableWidth,
yOffset + viewToDrawableHeight)
val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL)
} else {
val xOffset = mHorizontalFitOffsetPercent * (viewToDrawableWidth - drawableWidth) * scale
val yOffset = mVerticalFitOffsetPercent * (viewToDrawableHeight - drawableHeight) * scale
val drawableRect = RectF(
0f,
0f,
drawableWidth.toFloat(),
drawableHeight.toFloat())
val viewRect = RectF(xOffset, yOffset, xOffset + drawableWidth * scale, yOffset + drawableHeight * scale)
matrix.setRectToRect(drawableRect, viewRect, Matrix.ScaleToFit.FILL)
}
imageMatrix = matrix
}
}
Use in your layout as follows:
<your.package.OffsetImageView
android:id="#+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/image"
app:verticalFitOffset="0.3"
app:horizontalFitOffset="70%"
app:offsetScaleType="fitInside" />
This solution works fine.
A little improvement would make a CustomView customizable from .xml to topCrop or bottomCrop.
Here is the full solution on gitHub : ScalableImageView
val drawableRect = when (matrixType) {
FIT_BOTTOM -> RectF(0f, drawableHeight - offset, drawableWidth, drawableHeight)
FIT_TOP -> RectF(0f, 0f, drawableWidth, offset)
}
Did you try Imageview's Scaletype FIT_END Thts the best available option to show the end of a image.
I am designing an app that must look good on all android devices. In an activity i want to set the background. The image i want to use has an important figure in the right bottom corner
What I want:
- maintain aspect ratio
- right bottom corner of original image must be visible
- fullscreen
- must work on portrait and landscape
I have tried all the scaletype options, the fit options don't fill the entire screen and the centercrop crops at all sides (so it slices of a part of the right bottom corner).
First make an imageView for your drawable and customize it by changing <ImageView> to <com.packagename.CenterCropShiftsUp>, and set the scaleType to centerCrop.
Create CenterCropShiftsUp.java in the package I just mentioned, and use this code to shift the drawable upwards:
package nl.mijnverzekering.views;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;
public class CenterCropShiftsUp extends ImageView
{
public CenterCropShiftsUp(Context context, AttributeSet attrs)
{
super(context, attrs);
}
#Override
protected boolean setFrame(int l, int t, int r, int b)
{
int drawableWidth = getDrawable().getIntrinsicWidth();
int drawableHeight = getDrawable().getIntrinsicHeight();
int viewWidth = r - getPaddingLeft() - getPaddingRight();
int viewHeight = b - getPaddingTop() - getPaddingBottom();
float heightRatio = 1 / ((float) drawableHeight / (float) viewHeight);
float widthRatio = 1 / ((float) drawableWidth / (float) viewWidth);
// Choose the biggest ratio as scaleFactor
// (centerCrop does the same: the drawable never scales down to leave part of the screen empty)
float scale = heightRatio > widthRatio ? heightRatio : widthRatio;
int newDrawableHeight = (int) (scale * (float) drawableHeight);
// Shifts the t (top) of the imageFrame up (t -=)
// This calculation aligns the bottom of the drawable to the bottom of the screen
t -= (newDrawableHeight - b);
return super.setFrame(l, t, r, b);
}
}
It first calculates the scaleFactor of the image, then uses this scale to calculate the new drawableHeight (just as centerCrop would do it). With this height you can calculate how far the frame of the ImageView should be shifted upwards (using setFrame() to get the bottom of the drawable aligned to the bottom of the screen).
The alignment on the right, which you also requested, is of course automatically fixed due to the properties of centerCrop itself.
It seems to be a litte bit late, nevertheless I want to post my answer. I needed to have a top left shifted view, whereas the width is always cropped. I found this library (https://github.com/cesards/CropImageView), but I decided to just use portions of it. It ended up in overriding setFrameand setting the scale type to Matrix in the constructor of my custom image view.
#Override
protected boolean setFrame(int l, int t, int r, int b) {
boolean changed = super.setFrame(l, t, r, b);
int viewWidth = r - getPaddingLeft() - getPaddingRight();
int viewHeight = b - getPaddingTop() - getPaddingBottom();
if (viewHeight > 0 && viewWidth > 0) {
final Matrix matrixCopy = new Matrix();
matrixCopy.set(getImageMatrix());
final Drawable drawable = getDrawable();
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
float scaleY = (float) viewHeight / (float) drawableHeight;
float scaleX = (float) viewWidth / (float) drawableWidth;
float scale = scaleX > scaleY ? scaleX : scaleY;
matrixCopy.setScale(scale, scale);
setImageMatrix(matrixCopy);
}
return changed;
}
I´m currently testing out a "ScalingLinearLayout" that I found in a blog post.
http://www.quadra-tec.net/~floppie/blag/2013/01/scalinglinearlayout-auto-scaling-layouts-in-android/.
My issue is that the Main preview window of the graphical layout editor is black, while the other windows show a preview (see image below).
I´ve seen similar behavior earlier with custom components, but in my experience it is always the case that nothing is shown at all.
Has anyone seen this behavior before and knows what causes it?
Please also see the related code below
The sample layout in the image:
<com.nightfox.testapp.ScalingLinearLayout
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:background="#FFFFFF"
android:gravity="center"
android:orientation="vertical"
tools:context=".FlowA1" >
<RelativeLayout
android:layout_width="640px"
android:layout_height="960px"
android:gravity="center|bottom" >
<Button
android:id="#+id/button1"
android:layout_width="400px"
android:layout_height="200px"
android:layout_alignParentTop="true"
android:text="#string/no_set_alarm" />
<Button
android:id="#+id/button1"
android:layout_width="400px"
android:layout_height="200px"
android:layout_alignParentBottom="true"
android:text="#string/no_set_alarm" />
</RelativeLayout>
The custom ScalingLinearLayout looks like this:
package com.nightfox.testapp;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.LinearLayout;
public class ScalingLinearLayout extends LinearLayout {
int baseWidth;
int baseHeight;
boolean alreadyScaled;
float scale;
int expectedWidth;
int expectedHeight;
public ScalingLinearLayout(Context context) {
super(context);
Log.d("notcloud.view", "ScalingLinearLayout: width=" + this.getWidth() + ", height=" + this.getHeight());
this.alreadyScaled = false;
}
public ScalingLinearLayout(Context context, AttributeSet attributes) {
super(context, attributes);
Log.d("notcloud.view", "ScalingLinearLayout: width=" + this.getWidth() + ", height=" + this.getHeight());
this.alreadyScaled = false;
}
public void onFinishInflate() {
Log.d("notcloud.view", "ScalingLinearLayout::onFinishInflate: 1 width=" + this.getWidth() + ", height=" + this.getHeight());
// Do an initial measurement of this layout with no major restrictions on size.
// This will allow us to figure out what the original desired width and height are.
this.measure(1000, 1000); // Adjust this up if necessary.
this.baseWidth = this.getMeasuredWidth();
this.baseHeight = this.getMeasuredHeight();
Log.d("notcloud.view", "ScalingLinearLayout::onFinishInflate: 2 width=" + this.getWidth() + ", height=" + this.getHeight());
Log.d("notcloud.view", "ScalingLinearLayout::onFinishInflate: alreadyScaled=" + this.alreadyScaled);
Log.d("notcloud.view", "ScalingLinearLayout::onFinishInflate: scale=" + this.scale);
if(this.alreadyScaled) {
Scale.scaleViewAndChildren((LinearLayout)this, this.scale, 0);
}
}
public void draw(Canvas canvas) {
// Get the current width and height.
int width = this.getWidth();
int height = this.getHeight();
// Figure out if we need to scale the layout.
// We may need to scale if:
// 1. We haven't scaled it before.
// 2. The width has changed.
// 3. The height has changed.
if(!this.alreadyScaled || width != this.expectedWidth || height != this.expectedHeight) {
// Figure out the x-scaling.
float xScale = (float)width / this.baseWidth;
if(this.alreadyScaled && width != this.expectedWidth) {
xScale = (float)width / this.expectedWidth;
}
// Figure out the y-scaling.
float yScale = (float)height / this.baseHeight;
if(this.alreadyScaled && height != this.expectedHeight) {
yScale = (float)height / this.expectedHeight;
}
// Scale the layout.
this.scale = Math.min(xScale, yScale);
Log.d("notcloud.view", "ScalingLinearLayout::onLayout: Scaling!");
Scale.scaleViewAndChildren((LinearLayout)this, this.scale, 0);
// Mark that we've already scaled this layout, and what
// the width and height were when we did so.
this.alreadyScaled = true;
this.expectedWidth = width;
this.expectedHeight = height;
// Finally, return.
return;
}
super.draw(canvas);
}
}
and the Scale class:
package com.nightfox.testapp;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
public class Scale {
public static void scaleContents(View rootView, View container) {
Scale.scaleContents(rootView, container, rootView.getWidth(), rootView.getHeight());
}
// Scales the contents of the given view so that it completely fills the given
// container on one axis (that is, we're scaling isotropically).
public static void scaleContents(View rootView, View container, int width, int height) {
Log.d("notcloud.scale", "Scale::scaleContents: container: " + container.getWidth() + "x" + container.getHeight() + ".");
// Compute the scaling ratio
float xScale = (float)container.getWidth() / width;
float yScale = (float)container.getHeight() / height;
float scale = Math.min(xScale, yScale);
// Scale our contents
Log.d("notcloud.scale", "Scale::scaleContents: scale=" + scale + ", width=" + width + ", height=" + height + ".");
scaleViewAndChildren(rootView, scale, 0);
}
// Scale the given view, its contents, and all of its children by the given factor.
public static void scaleViewAndChildren(View root, float scale, int canary) {
// Retrieve the view's layout information
ViewGroup.LayoutParams layoutParams = root.getLayoutParams();
// Scale the View itself
if(layoutParams.width != ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.width != ViewGroup.LayoutParams.WRAP_CONTENT) {
layoutParams.width *= scale;
}
if(layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT && layoutParams.height != ViewGroup.LayoutParams.WRAP_CONTENT) {
layoutParams.height *= scale;
}
// If the View has margins, scale those too
if(layoutParams instanceof ViewGroup.MarginLayoutParams) {
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams)layoutParams;
marginParams.leftMargin *= scale;
marginParams.topMargin *= scale;
marginParams.rightMargin *= scale;
marginParams.bottomMargin *= scale;
}
root.setLayoutParams(layoutParams);
// Same treatment for padding
root.setPadding(
(int)(root.getPaddingLeft() * scale),
(int)(root.getPaddingTop() * scale),
(int)(root.getPaddingRight() * scale),
(int)(root.getPaddingBottom() * scale)
);
// If it's a TextView, scale the font size
/*
if(root instanceof TextView) {
TextView tv = (TextView)root;
tv.setTextSize(tv.getTextSize() * scale); //< We do NOT want to do this.
}
*/
// If it's a ViewGroup, recurse!
if(root instanceof ViewGroup) {
ViewGroup vg = (ViewGroup)root;
for(int i = 0; i < vg.getChildCount(); i++) {
scaleViewAndChildren(vg.getChildAt(i), scale, canary + 1);
}
}
}
}
This is because you have edited the theme of that particular xml file. you just need to click on your desire xml and change its theme from the theme option right in the center of the screen right side from the nexus S selection of yours... or u can goto res>values>strings and change the string responsible for theme selection.