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.
Related
I have an rotated textview and I want to drag and drop this view.
The problem is that the drag shadow has no rotation.
I found a solution for android in java but this does not work for me.
Maybe I translate the code wrong
How to drag a rotated DragShadow?
class CustomDragShdowBuilder : View.DragShadowBuilder
{
private View _view;
public CustomDragShdowBuilder(View view)
{
_view = view;
}
public override void OnDrawShadow(Canvas canvas)
{
double rotationRad = Math.ToRadians(_view.Rotation);
int w = (int)(_view.Width * _view.ScaleX);
int h = (int)(_view.Height * _view.ScaleY);
double s = Math.Abs(Math.Sin(rotationRad));
double c = Math.Abs(Math.Cos(rotationRad));
int width = (int)(w * c + h * s);
int height = (int)(w * s + h * c);
canvas.Scale(_view.ScaleX, _view.ScaleY, width / 2, height / 2);
canvas.Rotate(_view.Rotation, width / 2, height / 2);
canvas.Translate((width - _view.Width) / 2, (height - _view.Height) / 2);
base.OnDrawShadow(canvas);
}
public override void OnProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint)
{
shadowTouchPoint.Set(shadowSize.X / 2, shadowSize.Y / 2);
base.OnProvideShadowMetrics(shadowSize, shadowTouchPoint);
}
}
I found a solution for android in java but this does not work for me. Maybe I translate the code wrong
Yes, you are translating it wrong, you did change the codes of OnDrawShadow but you didn't pay attention to OnProvideShadowMetrics, which aims at changing the size of the canvas drawing area, so you need to pass the same width and height that has been calculated by codes:
Here is the modified version of DragShdowBuilder:
public class MyDragShadowBuilder : DragShadowBuilder
{
private int width, height;
// Defines the constructor for myDragShadowBuilder
public MyDragShadowBuilder(View v) : base(v)
{
}
// Defines a callback that sends the drag shadow dimensions and touch point back to the system.
public override void OnProvideShadowMetrics(Android.Graphics.Point outShadowSize, Android.Graphics.Point outShadowTouchPoint)
{
double rotationRad = Java.Lang.Math.ToRadians(View.Rotation);
int w = (int)(View.Width * View.ScaleX);
int h = (int)(View.Height * View.ScaleY);
double s = Java.Lang.Math.Abs(Java.Lang.Math.Sin(rotationRad));
double c = Java.Lang.Math.Abs(Java.Lang.Math.Cos(rotationRad));
//calculate the size of the canvas
//width = view's width*cos(rad)+height*sin(rad)
width = (int)(w * c + h * s);
//height = view's width*sin(rad)+height*cos(rad)
height = (int)(w * s + h * c);
outShadowSize.Set(width, height);
// Sets the touch point's position to be in the middle of the drag shadow
outShadowTouchPoint.Set(outShadowSize.X / 2, outShadowSize.Y / 2);
}
// Defines a callback that draws the drag shadow in a Canvas that the system constructs
// from the dimensions passed in onProvideShadowMetrics().
public override void OnDrawShadow(Canvas canvas)
{
canvas.Scale(View.ScaleX, View.ScaleY, width/2 , height/2);
//canvas.DrawColor(Android.Graphics.Color.White);
canvas.Rotate(View.Rotation,width/2, height / 2);
canvas.Translate((width - View.Width)/2, (height - View.Height) / 2);
base.OnDrawShadow(canvas);
}
}
And here is the complete sample:RotatedTextViewSample
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 am doing a Pong clone and I've implemented the ball as a Rect() object. Now to move the ball I use the Rect.offset(dx,dy), which offsets the ball by a specefied speed. For bouncing the ball off a wall I multiply the velocity of respective axis by -1. Now for bounce along the Y-axis , it bounces perfectly, but along the X-axis it starts behavioral weirdly. Is this some glitch with the android studio or am I doing something wrong?
package com.nblsoft.pong;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
public class PongLogic extends View {
//set screen constrains in dip
Configuration configuration = this.getResources().getConfiguration();
int dpHeight = configuration.screenHeightDp; //The current height of the available screen space, in dp units, corresponding to screen height resource qualifier.
int dpWidth = configuration.screenWidthDp; //The current width of the available screen space, in dp units, corresponding to screen width resource qualifier.
//int smallestScreenWidthDp = configuration.smallestScreenWidthDp; //The smallest screen size an application will see in normal operation, corresponding to smallest screen width resource qualifier.
//DisplayMetrics displayMetrics = this.getResources().getDisplayMetrics();
//float dpHeight = displayMetrics.heightPixels / displayMetrics.density;
//float dpWidth = displayMetrics.widthPixels / displayMetrics.density;
private int dptopixel(int DESIRED_DP_VALUE){
final float scale = getResources().getDisplayMetrics().density;
return (int)((DESIRED_DP_VALUE) * scale + 0.5f);
}
private int pixeltodp(int DESIRED_PIXEL_VALUE){
final float scale = getResources().getDisplayMetrics().density;
return (int) ((DESIRED_PIXEL_VALUE) - 0.5f / scale);
}
//set paddle size, speed, position vector
int AI_paddle_pos_x = 4 * (dptopixel(dpWidth)/100); //3 for 320x480, 10 for 1080x1920 etc.
int paddle_width = (dptopixel(dpWidth)/10); //
int AI_paddle_pos_y = (dptopixel(dpHeight)/10); //48 for 320x480, 190 for 1080x1920 etc.
int paddle_height = (dptopixel(dpHeight)/100) + 3; //the paddle is 100% of the total height of phone.
int user_paddle_pos_x = 4 * (dptopixel(dpWidth)/100) ;
int user_paddle_pos_y = dptopixel(dpHeight) - ((dptopixel(dpHeight)/10) + (dptopixel(dpHeight)/100) + 3) ;
//User Paddle
public Rect paddle_user = new Rect(user_paddle_pos_x,
user_paddle_pos_y,
user_paddle_pos_x + paddle_width,
user_paddle_pos_y + paddle_height);
//AI paddle
Rect paddle_AI = new Rect(AI_paddle_pos_x,
AI_paddle_pos_y,
AI_paddle_pos_x + paddle_width,
AI_paddle_pos_y + paddle_height);
//set ball position vector, Velocity vector, acceleration
int ball_pos_x = 0 ;
int ball_pos_y = (dptopixel(dpHeight)/2) ;
int ball_size = dptopixel(dpWidth)/100 ;
int ball_velocity = 3;
// Ball
Rect ball = new Rect(ball_pos_x,
ball_pos_y,
ball_pos_x+ball_size,
ball_pos_y+ball_size);
//Override onDraw method
#Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
Paint mytext = new Paint();
mytext.setColor(Color.WHITE);
//mytext.setStyle(Paint.Style.STROKE);
//mytext.setStrokeWidth(2);
// Draw Middle point
canvas.drawRect(0, ((dptopixel(dpHeight)) / 2), (dptopixel(dpWidth)), (((dptopixel(dpHeight)) / 2) + 2), mytext);
//draw both paddles
canvas.drawRect(paddle_user,mytext);
canvas.drawRect(paddle_AI, mytext);
//draw ball
canvas.drawRect(ball,mytext);
//Practise Methods
//canvas.drawText(Integer.toString(dptopixel(dpHeight)),300,300,mytext);
//canvas.drawText(Integer.toString(dptopixel(dpWidth)), 400, 400, mytext);
//canvas.drawText(Integer.toString(dpHeight),500,500,mytext);
//canvas.drawText(Integer.toString(dpWidth),600,600,mytext);
//canvas.drawText("Fuck", 700, 700, mytext);
//canvas.drawRect(0,0,dptopixel(dpWidth),dptopixel(dpHeight),mytext);
//Game Loop Updater
update();
invalidate();
}
private void update() {
if (ball.centerX() > (dptopixel(dpWidth))/2){
ball.offset(-ball_velocity,ball_velocity);
}
else{
ball.offset(ball_velocity,ball_velocity);
}
}
//Override Touch method
#Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
paddle_user.offset(10,0);
}
return true;
}
/* #Override
public boolean onTouch(View v, MotionEvent event) {
this.paddle_user.offsetTo(10,10);
return true; //Event Handled
}
*/
public PongLogic(Context context) {
super(context);
setBackgroundColor(Color.BLACK); //to set background
this.setFocusableInTouchMode(true); //to enable touch mode
}
}
Ok I've Figured out why the offset method was behaving erratically. First I had the original offset position in the else part of the conditional. Second I was negating the dx component only when the if evaluated to true. Which happened for just one frame because I was evaluating Rect object's X position. Which cause it to become false as soon as the -dx was subtracted from the ball's position.
To resolve it I had to place the offset out of the conditional and pass two separate values for the dx and dy. Then I tweaked the conditional so that it only assigned positive and negative values to the variables which were later passed into offset() function, Rather then calling the offset function itself.
This is the new update() method
private void update() {
if (ball.centerX() > (dptopixel(dpWidth))/2){
ball_velocity_x = -3;
}
else if (ball.centerY() > (dptopixel(dpHeight))) {
ball_velocity_y = -3;
}
ball.offset(ball_velocity_x, ball_velocity_y);
}
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;
}