2

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?

abdullah celik
  • 327
  • 2
  • 12

1 Answers1

0

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.

Eddy Talvala
  • 17,243
  • 2
  • 42
  • 47
  • 1
    does not work. In the Manifest file of the HdrViewFinder project mentioned above they have set the screentOrientation to landscape. Setting it to portrait + using your solution does not work. What I want is to use the same project but transform it to portrait mode. – abdullah celik Aug 24 '19 at 10:39
  • @EddyTalvala these issues with SurfaceView are not new (see, e.g. https://stackoverflow.com/q/33479004/192373). On the face of it, some devices can only work with TextureView in portrait orientation. – Alex Cohn Aug 26 '19 at 08:03
  • I am afraid you must use SurfaceTexture instead of SurfaceView. – Alex Cohn Aug 26 '19 at 08:33
  • I have created an [experimental fork](https://github.com/alexcohn/android-HdrViewfinder/tree/enable_portrait) that uses TextureView. At least, it handles Normal mode correctly. – Alex Cohn Aug 27 '19 at 10:39