I want to create a custom View, such that when it is inflated with wrap_content as one of the dimension parameters and match_parent as the other, it will have a constant aspect ratio, filling whichever dimension is set to match_parent, but providing the layout inflater with the other dimension to be "wrapped". I presume this is possible because, for example, a full screen width TextView would obviously be able to demand that it have space for two, three or any arbitrary number of lines of text (depending on width), but would not necessarily know this until inflation-time.
Ideally what I want to do is override layout methods in the View subclass such that when the view is inflated, I get the layout information, and supply my own dimensions for the "content" to be wrapped (ie my fixed-ratio rectangle).
I will need to create a lot of these custom views and put them in various different types of layout—sometimes using an Adapter—so really I want to have the maximum control over their inflation I can. What's the best technique for doing this?
You can always check for compliance to aspect ratio in onMeasure.
not a full answer I know, but it should lead you there ;)
I've now solved this with the following code. It's worth mentioning in passing that the class I'm overriding is a custom ViewGroup with custom children, all using the inherited onMeasure. The children are created and added at construction-time, and I would assume as a matter of course that this is necessary.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
float width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
float height = MeasureSpec.getSize(heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
float nominalHeight = getResources().getInteger(R.integer.nominalheight);
float nominalWidth = getResources().getInteger(R.integer.nominalwidth);
float aspectRatio = nominalWidth / nominalHeight;
if( widthMode == MeasureSpec.UNSPECIFIED ) { //conform width to height
width = height * aspectRatio;
}
else if (heightMode == MeasureSpec.UNSPECIFIED ) { //conform height to width
height = width / aspectRatio;
}
else if( width / height > aspectRatio //too wide
&& ( widthMode == MeasureSpec.AT_MOST )
) {
width -= (width - height * aspectRatio);
}
else if( width / height < aspectRatio //too tall
&& ( heightMode == MeasureSpec.AT_MOST )
) {
height -= (height - width / aspectRatio);
}
int newWidthMeasure = MeasureSpec.makeMeasureSpec((int)width, MeasureSpec.AT_MOST);
int newHeightMeasure = MeasureSpec.makeMeasureSpec((int)height, MeasureSpec.AT_MOST);
measureChildren(newWidthMeasure, newHeightMeasure);
setMeasuredDimension((int)width, (int)height);
}
I'm defining the aspect ratio in terms of a nominal rectangle in resources, but obviously there are plenty of other ways to do this.
With thanks to Josephus Villarey who pointed me at onMeasure(...) in the first place.
Related
I have a question about how the screen orientation in Android is handled when we use the Camera2 API in combination with SurfaceView.
I was playing with the official HdrViewfinder google sample code at https://github.com/googlesamples/android-HdrViewfinder a little bit. In that project, they use a class called FixedAspectSurfaceView which is an extension of SurfaceView.
But that project displays the camera preview correctly only when the screenOrientation of the activity (AndroidManifest) is in landscape mode, not in portrait mode. Setting the attribute to portrait swaps the preview in a weird way.
How could I modify that code to be also able to see the camera preview correctly in portrait mode ?
So, the FixedAspectSurfaceView.java class looks like this:
public class FixedAspectSurfaceView extends SurfaceView {
/**
* Desired width/height ratio
*/
private float mAspectRatio;
private GestureDetector mGestureDetector;
public FixedAspectSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
// Get initial aspect ratio from custom attributes
TypedArray a =
context.getTheme().obtainStyledAttributes(attrs,
R.styleable.FixedAspectSurfaceView, 0, 0);
setAspectRatio(a.getFloat(
R.styleable.FixedAspectSurfaceView_aspectRatio, 1.f));
a.recycle();
}
/**
* Set the desired aspect ratio for this view.
*
* #param aspect the desired width/height ratio in the current UI orientation. Must be a
* positive value.
*/
public void setAspectRatio(float aspect) {
if (aspect <= 0) {
throw new IllegalArgumentException("Aspect ratio must be positive");
}
mAspectRatio = aspect;
requestLayout();
}
/**
* Set a gesture listener to listen for touch events
*/
public void setGestureListener(Context context, GestureDetector.OnGestureListener listener) {
if (listener == null) {
mGestureDetector = null;
} else {
mGestureDetector = new GestureDetector(context, listener);
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
// General goal: Adjust dimensions to maintain the requested aspect ratio as much
// as possible. Depending on the measure specs handed down, this may not be possible
// Only set one of these to true
boolean scaleWidth = false;
boolean scaleHeight = false;
// Sort out which dimension to scale, if either can be. There are 9 combinations of
// possible measure specs; a few cases below handle multiple combinations
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
// Can't adjust sizes at all, do nothing
} else if (widthMode == MeasureSpec.EXACTLY) {
// Width is fixed, heightMode either AT_MOST or UNSPECIFIED, so adjust height
scaleHeight = true;
} else if (heightMode == MeasureSpec.EXACTLY) {
// Height is fixed, widthMode either AT_MOST or UNSPECIFIED, so adjust width
scaleWidth = true;
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
// Need to fit into box <= [width, height] in size.
// Maximize the View's area while maintaining aspect ratio
// This means keeping one dimension as large as possible and shrinking the other
float boxAspectRatio = width / (float) height;
if (boxAspectRatio > mAspectRatio) {
// Box is wider than requested aspect; pillarbox
scaleWidth = true;
} else {
// Box is narrower than requested aspect; letterbox
scaleHeight = true;
}
} else if (widthMode == MeasureSpec.AT_MOST) {
// Maximize width, heightSpec is UNSPECIFIED
scaleHeight = true;
} else if (heightMode == MeasureSpec.AT_MOST) {
// Maximize height, widthSpec is UNSPECIFIED
scaleWidth = true;
} else {
// Both MeasureSpecs are UNSPECIFIED. This is probably a pathological layout,
// with width == height == 0
// but arbitrarily scale height anyway
scaleHeight = true;
}
// Do the scaling
if (scaleWidth) {
width = (int) (height * mAspectRatio);
} else if (scaleHeight) {
height = (int) (width / mAspectRatio);
}
// Override width/height if needed for EXACTLY and AT_MOST specs
width = View.resolveSizeAndState(width, widthMeasureSpec, 0);
height = View.resolveSizeAndState(height, heightMeasureSpec, 0);
// Finally set the calculated dimensions
setMeasuredDimension(width, height);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (mGestureDetector != null) {
return mGestureDetector.onTouchEvent(event);
}
return false;
}
}
I changed the screenOrientation attribute in the AndroidManifest file to portrait.
I changed also the activity_main.xml layout file:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<com.celik.abdullah.project.utils.FixedAspectSurfaceView
android:id="#+id/preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
custom:aspectRatio="0.75"/>
<Button
android:id="#+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:text="next"/>
</FrameLayout>
When I leave the screenOrientation attribute in the manifest file in landscape, the camera preview is fine but the application opens of course "in landscape" mode. When I set the screenOrientation to portrait, then the camera preview "swaps the view to left". I did not know how to describe it but it is definitely weird. Why is the preview when I switch to portrait ?
And how could I modify the project so that it also can be used in portrait mode?
For FixedAspectSurfaceView, you should just be able to set its aspect ratio to the inverse of the aspect ratio you use in landscape. So if in landscape you set it to 4/3, set it to 3/4 for portrait layout.
The camera-to-SurfaceView path should handle all the rotations for you, you just need to keep the shape of the SurfaceView correct.
I am creating a custom view which extends LinearLayout. I'm adding some shapes to the linear layout, currently with a fixed value of space between them. I'm doing so by defining a LayoutParams and setting the margins to create the space between the shapes.
What I want to do is span them at an equal space across the entire screen so they would fill it, but only if the width of the view is set to either match_parent or fill_parent. If it's set to wrap_content then the original fixed value of space should be set.
I've tried doing:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
spaceBetweenShapesPixels = (parentWidth - shapeWidth * numberOfShapes) / numberOfShapess;
}
However, the method seems to be called twice - Once for the width and height of the parent and after that for the View's itself and when it's for the view itself, the space gets a 0 value.
So how can I just make this logic:
if(width is wrap_content)
{
space = 10;
}
else
{
space = (parentWidth - shapeWidth * numberOfShapes) / numberOfShapess;
}
You should use WidthMode and HeightMode
In the onMeasure() method. Below is sample a from one of my projects
#Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredWidth = 0, measuredHeight = 0;
if (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST) {
measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
}
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
} else if (heightMode == MeasureSpec.AT_MOST) {
double height = MeasureSpec.getSize(heightMeasureSpec) * 0.8;
measuredHeight = (int) height;// + paddingTop + paddingBottom;
}
}
MeasureSpec.EXACTLY - A view should be exactly this many pixels regardless of how big it actually wants to be.
MeasureSpec.AT_MOST - A view can be this size or smaller if it measures out to be smaller.
MeasureSpec.UNSPECIFIED - A view can be whatever size it needs to be in order to show the content it needs to show.
Refer this for more details https://stackoverflow.com/a/16022982/2809326
I know it's bit tricky and still thinking whether is it possible or not, but i want to make my image to adjust without decreasing in image quality on any android device when i used vector Drawable it was pretty convenient but sometimes size of vectors are not memory efficient so i don't want to use them.Though i want to know if there is any way to adjust simple PNG or JPEG files irrespective of resolution and screen size in Android?
If anyone can give me way,it would be great help !!
Use Resize Image View (Custom Image View)
public class ResizableImageView extends ImageView {
public ResizableImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ResizableImageView(Context context) {
super(context);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Drawable d = getDrawable();
// get drawable from imageview
if (d == null) {
super.setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
return;
}
int imageHeight = d.getIntrinsicHeight();
int imageWidth = d.getIntrinsicWidth();
// get height and width of the drawable
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// get width and height extracts the size from the supplied measure specification.
float imageRatio = 0.0F;
if (imageHeight > 0) {
imageRatio = imageWidth / imageHeight;
}
float sizeRatio = 0.0F;
if (heightSize > 0) {
sizeRatio = widthSize / heightSize;
}
int width;
int height;
if (imageRatio >= sizeRatio) {
// set width to maximum allowed
width = widthSize;
// scale height
height = width * imageHeight / imageWidth;
} else {
// set height to maximum allowed
height = heightSize;
// scale width
width = height * imageWidth / imageHeight;
}
setMeasuredDimension(width, height);
// This method must be called to store the measured width and measured height. Failing to do so will trigger an exception at measurement time
}
}
I've created a custom View that implements onMeasure:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
float width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
float height = MeasureSpec.getSize(heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
float nominalHeight = getResources().getInteger(R.integer.nominalheight);
float nominalWidth = getResources().getInteger(R.integer.nominalwidth);
float aspectRatio = nominalWidth / nominalHeight;
if( width / height > aspectRatio //too wide
&& (
widthMode == MeasureSpec.AT_MOST ||
widthMode == MeasureSpec.UNSPECIFIED
)
) {
width -= (width - height * aspectRatio);
}
if( width / height < aspectRatio //too tall
&& (
heightMode == MeasureSpec.AT_MOST ||
heightMode == MeasureSpec.UNSPECIFIED
)
) {
height -= (height - width / aspectRatio);
}
setMeasuredDimension((int)width, (int)height);
}
The intention is, where possible, to create a rectangle whose aspect ratio is the same as the one specified by nominalheight and nominalwidth. Obviously if the function is passed MeasureSpec.EXACTLY then it should lay out with the dimension given in that direction.
I'm laying this View out with WRAP_CONTENT in the xml in both directions. What's puzzling me is that, stepping through the onMeasure calls, half of them show MeasureSpec.AT_MOST and the routine calculates the correct rectangle, and the other half show MeasureSpec.EXACTLY and it obviously uses the given dimensions. Even more puzzlingly, if I disable the conditionality (I would assume, from documentation and the example code, incorrectly) it works fine.
Why am I getting these alternating calls with different values, and how can I persuade Android to lay my View out in the correct dimensions?
I would like a view styled similar to the album art in the Rdio app (img)
I don't know how I should be going about this. I want to load a bitmap into the view, if that affects the solution at all.
In this case (where the control has the full width of the device) you can always just use getWindowManager().getDefaultDisplay().getWidth(); to get the width and then manually set the height.
If you're not using the full display width then you'll probably need to override onMeasure in the View class and set the height to match the width in there, perhaps like this:
#Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec )
{
viewWidth = MeasureSpec.getSize( widthMeasureSpec );
viewHeight = MeasureSpec.getSize( heightMeasureSpec );
if(viewWidth != 0) viewHeight = viewWidth;
setMeasuredDimension( viewWidth, viewHeight );
}