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.
Related
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'm implementing Camera 2 API in my project. I'm using TextureView and these line of codes to set the camera fullscreen preview size:
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
mPreviewSize = map.getOutputSizes(SurfaceTexture.class)[0];
This seems to be the largest preview size that device support. I'm not sure if this size works with all devices and fit its device's aspect ratio without being stretched. Does anyone know?
If your Camera resolutions , texture view and your device's display dimensions are not same then you have to adjust the dimensions. For that you have to put your TextureView inside of FrameLayout. Below Code is applicable to all the devices with various Display resolutions.
Take your Display Dimetions if you are previewing in full screen.Take int DSI_height, int DSI_width global variable.
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
DSI_height = displayMetrics.heightPixels;
DSI_width = displayMetrics.widthPixels;
select your required resolutions from Camera2 API and assign to Size imageDimension, Take private Size imageDimension globally and use
setAspectRatioTextureView(imageDimension.getHeight(),imageDimension.getWidth());
and use below logic
private void setAspectRatioTextureView(int ResolutionWidth , int ResolutionHeight )
{
if(ResolutionWidth > ResolutionHeight){
int newWidth = DSI_width;
int newHeight = ((DSI_width * ResolutionWidth)/ResolutionHeight);
updateTextureViewSize(newWidth,newHeight);
}else {
int newWidth = DSI_width;
int newHeight = ((DSI_width * ResolutionHeight)/ResolutionWidth);
updateTextureViewSize(newWidth,newHeight);
}
}
private void updateTextureViewSize(int viewWidth, int viewHeight) {
Log.d(TAG, "TextureView Width : " + viewWidth + " TextureView Height : " + viewHeight);
textureView.setLayoutParams(new FrameLayout.LayoutParams(viewWidth, viewHeight));
}
There might be edge cases where that approach would fail, but I don't have a perfect answer to your question why.
In contrast, I have a proper approach on how to implement a version that will most certainly work:
Looking at the Google API demos for the Camera 2, I found some sample code that should be helpful to you to make sure it will fit all screen sized correctly:
/**
* Given {#code choices} of {#code Size}s supported by a camera, choose the smallest one that
* is at least as large as the respective texture view size, and that is at most as large as the
* respective max size, and whose aspect ratio matches with the specified value. If such size
* doesn't exist, choose the largest one that is at most as large as the respective max size,
* and whose aspect ratio matches with the specified value.
*
* #param choices The list of sizes that the camera supports for the intended output
* class
* #param textureViewWidth The width of the texture view relative to sensor coordinate
* #param textureViewHeight The height of the texture view relative to sensor coordinate
* #param maxWidth The maximum width that can be chosen
* #param maxHeight The maximum height that can be chosen
* #param aspectRatio The aspect ratio
* #return The optimal {#code Size}, or an arbitrary one if none were big enough
*/
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth,
int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio) {
// Collect the supported resolutions that are at least as big as the preview Surface
List<Size> bigEnough = new ArrayList<>();
// Collect the supported resolutions that are smaller than the preview Surface
List<Size> notBigEnough = new ArrayList<>();
int w = aspectRatio.getWidth();
int h = aspectRatio.getHeight();
for (Size option : choices) {
if (option.getWidth() <= maxWidth && option.getHeight() <= maxHeight &&
option.getHeight() == option.getWidth() * h / w) {
if (option.getWidth() >= textureViewWidth &&
option.getHeight() >= textureViewHeight) {
bigEnough.add(option);
} else {
notBigEnough.add(option);
}
}
}
// Pick the smallest of those big enough. If there is no one big enough, pick the
// largest of those not big enough.
if (bigEnough.size() > 0) {
return Collections.min(bigEnough, new CompareSizesByArea());
} else if (notBigEnough.size() > 0) {
return Collections.max(notBigEnough, new CompareSizesByArea());
} else {
Log.e(TAG, "Couldn't find any suitable preview size");
return choices[0];
}
}
Source
Also you should take a look at the whole Camera2BasicFragment.java and AutoFitTextureView.java classes for proper implementation.
I solved this problem via a different approach. I get the screen width and height and calculate how much wider or higher the preview would have to be to fill the whole screen and keep aspect ratio. It works pretty well for me without any distortions.
Add a class member variable:
public DisplayMetrics mMetrics = new DisplayMetrics();
Use the following as onMeasure:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height);
} else {
WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getMetrics(mMetrics);
double ratio = (double)mRatioWidth / (double)mRatioHeight;
double invertedRatio = (double)mRatioHeight / (double)mRatioWidth;
double portraitHeight = width * invertedRatio;
double portraitWidth = width * (mMetrics.heightPixels / portraitHeight);
double landscapeWidth = height * ratio;
double landscapeHeight = (mMetrics.widthPixels / landscapeWidth) * height;
if (width < height * mRatioWidth / mRatioHeight) {
setMeasuredDimension((int)portraitWidth, mMetrics.heightPixels);
} else {
setMeasuredDimension(mMetrics.widthPixels, (int)landscapeHeight);
}
}
}
Any feedback is greatly appreciated ;)
Best M
Change the AutoFitTextureView.java file and set value like below:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height);
} else {
if (width < height * mRatioWidth / mRatioHeight) {
setMeasuredDimension(width, height);
Log.d("rlijeolid1",String.valueOf(width)+"\t"+String.valueOf(height));
} else {
setMeasuredDimension(width , height);
Log.d("rlijeolid2",String.valueOf(height * mRatioWidth / mRatioHeight)+"\t"+String.valueOf(height));
}
}
}
I am creating an application that takes an image and centers it onscreen for alteration. I am using a SurfaceView and Canvas. I am trying to center the SurfaceView in the parent RelativeLayout. I had it working, but it would become distorted on rotate. So I am currently using the surfaceChanged method to reset the size of the bitmap and canvas when the orientation changes:
#Override
public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
bitmap_width = height * imageRatio;
bitmap_height = height;
} else {
bitmap_width = width;
bitmap_height = width / imageRatio;
}
canvas_width = (int) bitmap_width;
canvas_height = (int) bitmap_height;
try {
canvas_bitmap = Bitmap.createScaledBitmap(initial_bitmap, (int) bitmap_width, (int) bitmap_height, true);
surface_canvas = new Canvas();
surface_canvas.setBitmap(canvas_bitmap);
} catch (Exception exception) {
exception.printStackTrace();
}
}
The scaling now works, but now it is not centered. I also noticed that the rest of the background is black, not gray as it used to be. This would lead me to believe that the canvas is stretched beyond the view of the bitmap. Upon further inspection I noticed that the surfaceChanged method gets called twice when started. So I started Logging everything, and this is what I found:
07-27 16:39:21.363 2129-2129/com... E/SURFACE_CHANGED﹕ imageRatio: 1.3333334 width: 1080 height: 1701 bitmap_width: 1080.0 bitmap_height: 810.0 canvas_width: 1080 canvas_height: 810 surface_width: -1 surface_height: -2
07-27 16:39:21.388 2129-2129/com... E/SURFACE_CHANGED﹕ imageRatio: 0.0 width: 1080 height: 1701 bitmap_width: 1080.0 bitmap_height: Infinity canvas_width: 1080 canvas_height: 2147483647 surface_width: -1 surface_height: -2
Most importantly the imageRatio, which is only set once in my code, changes to 0.0. Thus setting one either the height or the width of the bitmap to Infinity. If I rotate the device, a similar set of two logs appear, with the original imageRatio set to 1.33... and then changing to 0.0. I cannot for the life of me figure out why this would be the case.
Anyway, this is easily avoided. I wrapped my code it an if statement specifying (imageRatio > 0.0). Yet the issue persists, nothing changes!
Okay, I figured it out. The width and the height of the actual SurfaceView was not changing to match the size of the bitmap, so I had to change the size of the SurfaceView before surfaceChanged got called. So I overrode onConfigurationChanged to change the size of the SurfaceView itself with surfaceHolder.setFixedSize(), which ends up calling surfaceChanged anyway:
#Override
public void onConfigurationChanged(Configuration config) {
super.onConfigurationChanged(config);
int screenWidth = screenSize.x;
int screenHeight = screenSize.y;
float width;
float height;
if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
screenHeight = screenSize.x;
width = screenHeight * imageRatio;
height = screenHeight;
} else if (config.orientation == Configuration.ORIENTATION_PORTRAIT) {
screenWidth = screenSize.x;
width = screenWidth;
height = screenWidth / imageRatio;
} else {
Log.e("UNSUPPORTED_ORIENTATION", Integer.toString(config.orientation));
width = screenWidth;
height = screenHeight;
}
surfaceHolder.setFixedSize((int) width, (int) height);
}
Then I used surfaceChanged to set the width and height of the canvas and bitmap to match the surfaceView:
#Override
public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) {
bitmapWidth = width;
bitmapHeight = height;
canvasWidth = width;
canvasHeight = height;
try {
canvasBitmap = Bitmap.createScaledBitmap(initialBitmap, (int) bitmapWidth, (int) bitmapHeight, true);
surfaceCanvas = new Canvas();
surfaceCanvas.setBitmap(canvasBitmap);
} catch (Exception exception) {
exception.printStackTrace();
}
}
Here's the scenario: I've some info to show on a ListView. Every row can have, title, body, date, avatar, and more data. And a ImageView that comes from network. The ImageView has a different height on every row. I know what will be it's height in px once downloaded.
What I'm doing right now is to adjust the width of the image to fill it's parent, and auto adjusting the it's height automatically.
I'm loading a fixed placeholder while the image is downloaded.
Here's the code of the ImageView:
public class ResizableImageView2 extends ImageView {
public ResizableImageView2(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
Drawable d = getDrawable();
if(d!=null){
// ceil not round - avoid thin vertical gaps along the left/right edges
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = (int) Math.ceil((float) width * (float) d.getIntrinsicHeight() / (float) d.getIntrinsicWidth());
setMeasuredDimension(width, height);
}else{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
The problem is: The placeholder is loaded with a fixed size, and once downloaded we put the ImageView, the layout recalculates it self, and that causes wasted cpu, slow scrolling.
I'm already pre-caching the incoming rows to "minimize" this problem. This way the app loads an image from disk/memory 80% of the time (if you are in a good network and you scroll in a normal speed) and the listview does not "flicker".
The solution I'm searching is to preset the size of the placeholder at the same size that will be the downloaded image. But for some reason I'm having a hard time doing this.
I can make a little cropping of the image (some small pixels) if needed, but nothing as make all the images croped at the same size :P
Ideas? examples?
Finally for me the solution was:
public class ResizableImageView2 extends ImageView {
private int fixedHeight = -1;
private int fixedWidth = -1;
public ResizableImageView2(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (BuildConfig.DEBUG) Log.e("ResizableImageView2", "onMeasure called!");
if (fixedHeight != -1 && fixedWidth != -1) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = (int) Math.ceil((float) width * (float) fixedHeight / (float) fixedWidth);
setMeasuredDimension(width, height);
} else {
Drawable d = getDrawable();
if (d != null) {
// ceil not round - avoid thin vertical gaps along the left/right edges
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = (int) Math.ceil((float) width * (float) d.getIntrinsicHeight() / (float) d.getIntrinsicWidth());
setMeasuredDimension(width, height);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
public void setFixedHeight(int fixedHeight) {
this.fixedHeight = fixedHeight;
}
public void setFixedWidth(int fixedWidth) {
this.fixedWidth = fixedWidth;
}
I can set the height and the width before downloading the bitmap with setFixedHeight and setFixedWidth. The place holder should be a 9 patch for better stretching. and at the layout I've android:scaleType="fitXY".
With this I can pre-set the size of the imageview before downloading it, and once downloaded the image view will fill the width and have a height with the correct aspect ratio.
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.