I'm trying to get the new Camera2 working with a simple SurfaceView and I'm having some problems with the live preview. On some devices the image is stretched out of proportions while looking fine on others.
I've setup a SurfaceView that I programatically adjust to fit the size of the preview stream size.
On Nexus 5 this looks fine but one Samsung devices its way off. Also the Samsung devices have a black border on the right part of the preview.
Is it really not possible to work with the SurfaceView or is this the time to switch to TextureView ?
Yes, it is certainly possible. Note that the SurfaceView and its associated Surface are two different things, and each can/must be assigned a size.
The Surface is the actual memory buffer which will hold the output of the camera, and thus setting its size dictates the size of the actual image you will get from each frame. For each format available from the camera, there is a small set of possible (exact) sizes you can make this buffer.
The SurfaceView is what does the displaying of this image when it is available, and can basically be any size in your layout. It will stretch its underlying associated image data to fit whatever its layout size is, but note this display size is different from the data's size- Android will resize the image data for display automatically. This is what is probably causing your stretching.
For example, you can make a SurfaceView-based autofit View similar to the camera2basic's AutoFitTextureView as follows (this is what I use):
import android.content.Context;
import android.util.AttributeSet;
import android.view.SurfaceView;
public class AutoFitSurfaceView extends SurfaceView {
private int mRatioWidth = 0;
private int mRatioHeight = 0;
public AutoFitSurfaceView(Context context) {
this(context, null);
}
public AutoFitSurfaceView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoFitSurfaceView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
* calculated from the parameters. Note that the actual sizes of parameters don't matter, that
* is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
*
* #param width Relative horizontal size
* #param height Relative vertical size
*/
public void setAspectRatio(int width, int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Size cannot be negative.");
}
mRatioWidth = width;
mRatioHeight = height;
requestLayout();
}
#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, width * mRatioHeight / mRatioWidth);
} else {
setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
}
}
}
}
In the docs for createCaptureSession it says
For drawing to a SurfaceView: Once the SurfaceView's Surface is
created, set the size of the Surface with setFixedSize(int, int) to be
one of the sizes returned by getOutputSizes(SurfaceHolder.class)
I've never worked with Camera2 (but very interesting in the new technology) and all I can suggest you is to follow the samples. And if you check it carefully, the sample uses a Custom TextureView, you should probalby try to copy it to your project:
http://developer.android.com/samples/Camera2Basic/src/com.example.android.camera2basic/AutoFitTextureView.html
/**
* A {#link TextureView} that can be adjusted to a specified aspect ratio.
*/
public class AutoFitTextureView extends TextureView {
also considering that different between SurfaceView and TextureView (theoretically) is just that you can use as a normal view in layout and the other "punches a whole" through the view hierarchy, and that Camera2 is only available in API21+... there should be no harm in migrating to TextureView
Related
Following are the screenshots when using texture view in camera2 apis.In full screen the preview stretches,but it works when using lower resolution(second image).
How to use this preview in full screen without stretching it.
Below answer assumes you are in portrait mode only.
Your question is
How to use the preview in full-screen without stretching it
Let's break it down to 2 things:
You want the preview to fill the screen
The preview cannot be distorted
First you need to know that this is logically impossible without crop, if your device's viewport has a different aspect ratio with any available resolution the camera provides.
So I would assume you accept cropping the preview.
Step 1: Get a list of available resolutions
StreamConfigurationMap map = mCameraCharacteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map == null) {
throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
}
Size[] sizes = map.getOutputSizes(SurfaceTexture.class);
Now you get a list of available resolutions (Sizes) of your device's camera.
Step 2: Find the best aspect ratio
The idea is to loop the sizes and see which one best fits. You probably need to write your own implementation of "best fits".
I am not going to provide any code here since what I have is quite different from your use case. But ideally, it should be something like this:
Size findBestSize (Size[] sizes) {
//Logic goes here
}
Step 3: Tell the Camera API that you want to use this size
//...
textureView.setBufferSize(bestSize.getWidth(), bestSize.getHeight());
Surface surface = textureView.getSurface();
try {
mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);
mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
mSessionCallback, null);
} catch (final Exception e) {
//...
}
Step 4: Make your preview extends beyond your viewport
This is then nothing related to the Camera2 API. We "crop" the preview by letting the SurfaceView / TextureView extends beyond device's viewport.
First place your SurfaceView or TextureView in a RelativeLayout.
Use the below to extend it beyond the screen, after you get the aspect ratio from step 2.
Note that in this case you probably need to know this aspect ratio before you even start the camera.
//Suppose this value is obtained from Step 2.
//I simply test here by hardcoding a 3:4 aspect ratio, where my phone has a thinner aspect ratio.
float cameraAspectRatio = (float) 0.75;
//Preparation
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int screenWidth = metrics.widthPixels;
int screenHeight = metrics.heightPixels;
int finalWidth = screenWidth;
int finalHeight = screenHeight;
int widthDifference = 0;
int heightDifference = 0;
float screenAspectRatio = (float) screenWidth / screenHeight;
//Determines whether we crop width or crop height
if (screenAspectRatio > cameraAspectRatio) { //Keep width crop height
finalHeight = (int) (screenWidth / cameraAspectRatio);
heightDifference = finalHeight - screenHeight;
} else { //Keep height crop width
finalWidth = (int) (screenHeight * cameraAspectRatio);
widthDifference = finalWidth - screenWidth;
}
//Apply the result to the Preview
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) cameraView.getLayoutParams();
lp.width = finalWidth;
lp.height = finalHeight;
//Below 2 lines are to center the preview, since cropping default occurs at the right and bottom
lp.leftMargin = - (widthDifference / 2);
lp.topMargin = - (heightDifference / 2);
cameraView.setLayoutParams(lp);
If you don't care about the result of Step 2, you can actually ignore Step 1 to Step 3 and simply use a library out there, as long as you can configure its aspect ratio. (It looks like this one is the best, but I haven't tried yet)
I have tested using my forked library. Without modifying any code of my library, I managed to make the preview fullscreen just by using Step 4:
Before using Step 4:
After using Step 4:
And the preview just after taking a photo will not distort as well, because the preview is also extending beyond your screen.
But the output image will include area that you cannot see in the preview, which makes perfect sense.
The code of Step 1 to Step 3 are generally referenced from Google's CameraView.
That's a common problem on some devices. I've noticed it mostly on samsung. You may use a trick with setting transformation on your TextureView to make it centerCrop like ImageView behaviour
I also faced similar situation, but this one line solved my problem
view_finder.preferredImplementationMode = PreviewView.ImplementationMode.TEXTURE_VIEW
in your xml:
<androidx.camera.view.PreviewView
android:id="#+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
For camera implementation using cameraX you can refer
https://github.com/android/camera-samples/tree/master/CameraXBasic
I figured out what was your poroblem. You were probably trying something like this:
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
#Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int j) {
cam.startPreview(surfaceTexture, i, j);
cam.takePicture();
}
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) { }
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { return false; }
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { }
});
I am using recyclerview with imageviews in each cell.Each imageview loads images from the web and can be square or with more width than height or more height than width i.e any sizes.I am going to display a placeholder for each image while it loads in the background(with a progressbar).But the problem is the dimension of the images is unknown and I want to size the placeholders exactly the size of the images like the 9gag app in which the placeholders are exactly the size of the images while loading in the bacground.How do I achieve this in android ?I don't want to use wrap-content(produces a jarring effect after the image has been downloaded) or a specific height to the imageviews(crops the images).I am using UIL and currently planning to switch to Fresco or Picassa.
In case that your placeholder is just filled of some color, you can easily emulate a color drawable with exactly the same size.
/**
* The Color Drawable which has a given size.
*/
public class SizableColorDrawable extends ColorDrawable {
int mWidth = -1;
int mHeight = -1;
public SizableColorDrawable(int color, int width, int height) {
super(color);
mWidth = width;
mHeight = height;
}
#Override public int getIntrinsicWidth() {
return mWidth;
}
#Override public int getIntrinsicHeight() {
return mHeight;
}
}
To use it with Picasso:
Picasso.with(context).load(url).placeholder(new SizableColorDrawable(color, width, height)).into(imageView);
Now some tips on the ImageView:
public class DynamicImageView extends ImageView {
#Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Drawable drawable = getDrawable();
if (drawable == null) {
super.onMeasure(widthSpecureSpec, heightMeasureSpec);
return;
}
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int reqWidth = drawable.getIntrinsicWidth();
int reqHeight = drawable.getIntrinsicHeight();
// Now you have the measured width and height,
// also the image's width and height,
// calculate your expected width and height;
setMeasuredDimension(targetWidth, targetHeight);
}
}
Hope this will help someone..
If you use fresco, you need to pass the width and height of your image from the web server, then you set the layout params of your drawee view with that width and height.
Like this:
RelativeLayout.LayoutParams draweeParams =
new RelativeLayout.LayoutParams(desiredImageWidthPixel,
desiredImageHeightPixel);
yourDraweeView.setLayoutParams(draweeParams);
From width and height of the image that you pass from your web server, you can calculate/resize proportionally the view as you need. Where desiredImageWidthPixel is the calculated image width that you want to show in yourDraweeView and desiredImageHeightPixel is the calculated image height that you want to show in yourDraweeView.
Don't forget to call
yourDraweeView.getHierarchy().setActualImageScaleType(ScalingUtils.ScaleType.FIT_XY);
To make yourDraweeView match the actual params that you set before.
Hope this helps
You can feed image dimensions along with the image url. (I assume you are getting image list from a source, like a JSON file or something.) And resize the ImageView holder inside the RecyclerView according to their dimension and then fire the image downloading process.
I have found out that when using a textureview instead of a surfaceview as a camera preview (both hooked up to the camera via a mediarecorder) then the preview is much more fuzzy.
What I mean by fuzzy is that in a texture view you can see the pixels, especially when zooming. That is not the case when using a surfaceview. Why is that the case?
UPD:
Sorry,but after I re-write my shit code, the key is the preview size too small that caused "fuzziness", so you should set a reasonable preview Size,not the reason strikeout below, but auto-focus is suggested ...
Size size = getBestSupportSize(parameters.getSupportedPreviewSizes(), width, height);
parameters.setPreviewSize(size.width, size.height);
As to the method getBestSupportSize(), how to get the bestSize for your project needs, in this case, it is as large as the screen width andhe ratio is 4/3 your's may be some other, I calculate the ration dividing width/height.
private Size getBestSupportSize(List<Size> sizes, int width, int height) {
Size bestsize = sizes.get(0);
int screenWidth = getResources().getDisplayMetrics().widthPixels;
int dt = Integer.MAX_VALUE;
for (int i = sizes.size() - 1; i >= 0; i--) {
Log.d(TAG, "-index : " + i);
Size s = sizes.get(i);
if (s.width * 3.0f / 4 == s.height) {
int newDT = Math.abs(screenWidth - s.width);
if (newDT < dt && screenWidth < s.width) {
dt = newDT;
bestsize = s;
}
}
}
return bestsize;//note that if no "4/3" size supported,default return size[0]
}
So this "fuzziness" was caused by a small previewSize calcualate a best size for the camera using this getSupportedPreviewSizes() method
And I will keep the autoFocus snippet below, strikeout though, FYR if is needed.
Well i got the solution for this "fuzzy" problem,and my case is just using TextureView andsurfaceTexture to take a pic instead of old surfaceView withsurfaceHolderway.
The key is set this mCamera.autofocus(), why the pic is"fuzzy" is bacause we lack of this autoFocus setting.
like below :
mCamera.setPreviewTexture(surface);
//enable autoFocus if moving
mCamera.setAutoFocusMoveCallback(new AutoFocusMoveCallback() {
#Override
public void onAutoFocusMoving(boolean start, Camera camera) {
if (start) { //true means you are moving the camera
mCamera.autoFocus(myAutoFocus);
}
}
});
mCamera.startPreview();
The autoFocusCallback like this:
AutoFocusCallback myAutoFocus = new AutoFocusCallback() {
#Override
public void onAutoFocus(boolean success, Camera camera) {
}
};
Say you design for a normal screen(4 inch) of hdpi density, but then when you see it on a large screen(7 inch) of hdpi density the icons are too small.
Initially what I did was have to create a folder drawable-large-hdpi, then take the icons from xhdpi and copy it in there.
It seemed to work well, but then when I came across a large xhdpi screen it caused problems because Android was looking in the drawable-large-hdpi first before looking in drawable-xhdpi folder, because large has precedence. Creating another folder drawable-large-xhdpi and copying files seems inefficient as you have to do this for the ever expanding dpi screens.
So rather than having "wrap_content" on your icons inside the app should you be setting a particular width/height e.g. layout_width/height=#dimen/icon_size, then in the dimensions.xml set different widths in the values and values-large folder?
It would be nice if one could set it as a percentage of the parent view, but layout-weight only works for certain scenarios of linear-layouts
Are there better approaches.
public class ScreenPercentImageView extends ImageView {
private float mWidthPercent = 100;
private double mDrawableScalePercent;
public ScreenPercentImageView(Context context) {
super(context);
}
public ScreenPercentImageView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
setup(context, attrs);
}
public ScreenPercentImageView(Context context, AttributeSet attrs) {
super(context, attrs);
setup(context, attrs);
}
private void setup(Context context, AttributeSet attrs) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.ScreenPercentImageView, 0, 0);
try {
mWidthPercent = a.getFloat(
R.styleable.ScreenPercentImageView_widthPercentIV, 50);
} finally {
a.recycle();
}
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentWidth = getResources().getDisplayMetrics().widthPixels;
int parentHeight = getResources().getDisplayMetrics().heightPixels;
int width;
if (parentHeight > parentWidth)
width = (int) (parentWidth * mWidthPercent / 100);
else
width = (int) (parentHeight * mWidthPercent / 100);
mDrawableScalePercent = (double) width / parentWidth;
int height = getDrawable().getIntrinsicHeight() * width
/ getDrawable().getIntrinsicWidth();
setMeasuredDimension(width, height);
}
public double getDrawableScalePercent() {
return mDrawableScalePercent;
}
}
This is an ImageView that you can use as an example. I created it so it can use the width as a percentage of the screen size. It will scale the height appropriately. You can use this concept to have it use the width of the parent instead of the screen size.
Your layouts used on bigger screens, should no be just "stretched-out"/enlarged versions of the smaller screen ones. Android is great at loading and creating different layouts, according to how much free space you have on your screen.
Fragments were introduced to Android, so that you can do all kinds of stuff, in order to utilize the different screen sizes to the fullest.
Use the smallest width availabe bucket (layout-swXdp where X is the number of dp you like) and create unique layouts when the screen has enough space inside, and your tablet users will love you
More info here: Building a Dynamic UI with Fragments
I am using this camera preview application for my Android app.
I want the camera preview over the full screen.Hence I used the example from Android APIs to try setting the preview to full screen. This is how I am trying to do it:
if (!cameraConfigured) {
Camera.Parameters parameters=camera.getParameters();
Camera.Size size = getOptimalPreviewSize(mSupportedPreviewSizes, width, height);
if (size != null) {
parameters.setPreviewSize(size.width, size.height);
camera.setParameters(parameters);
cameraConfigured=true;
}
I am using relative layout as my layout. My layout settings are follows:
<android.view.SurfaceView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/preview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
I am still not able to preview my camera over the entire screen. I would like to know how to preview over the entire screen.
I found the problem. I added the following setting to Android Manifest file
<supports-screens android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" android:xlargeScreens="true"/>
I am able to view the camera over full screen.
Im assuming your code is within a class that extends SurfaceView and that you will place the surfaceView inside a FrameLayout that is as large as the display.
What you are not doing in your code is setting your SurfaceView to be the same size as the display which in the following code block is done by getting the layout, and setting the width and height.
I use the following code to stretch to the dimensions of the screen:
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
...
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
...
final DisplayMetrics dm = this.getResources().getDisplayMetrics();
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(cameraSize.width, cameraSize.height);
mCamera.setParameters(parameters);
//Here you get the SurfaceView layout and subsequently set its width and height
FrameLayout.LayoutParams frameParams = (FrameLayout.LayoutParams) this.getLayoutParams();
frameParams.width = LayoutParams.MATCH_PARENT;// dm.widthPixels should also work
frameParams.height = LayoutParams.MATCH_PARENT;//dm.heightPixels should also work
this.setLayoutParams(frameParams);
//And we should have things set now...
....
}
...
}
I hope this helps. The key parts to integrate are between those inner comments
Get dimensions of the screen
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
int width = displayMetrics.widthPixels;
use this width and height to get preview size.
First, get the display width and height:
int dw = currentDisplay.getWidth(); // dw == display width
int dh = currentDisplay.getHeight(); // dh == display height
Then loop through the supported preview sizes, and pick the largest one where, if
(size.width <= dw && size.height <= dh). When you hit one where this if statement
fails, use the previous values (you could set something like prevWidth and prevHeight
as a quick/easy way to step back one ... just remember to set each of those AFTER
you test them; after you hit a preview size that's larger than screen size, just
break out of the loop.
Later,
--jim