PreviewViewMeteringPointFactory.java

/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.camera.view;

import android.graphics.Point;
import android.graphics.PointF;
import android.util.Size;
import android.view.Display;
import android.view.Surface;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.MeteringPointFactory;
import androidx.camera.view.PreviewView.ScaleType;

/**
 * Implement the MeteringPointFactory for PreviewView by DisplayOrientedMeteringPointFactory.
 *
 * <p>The width / height in DisplayOrientedMeteringPointFactory defines area (0, 0) - (width,
 * height) which represents the full preview surface.  The (x, y) passed to createPoint()
 * must be in this coordinate system.
 *
 * However, in PreviewView, the preview could be cropped or letterbox/pillarbox depending on
 * the scaleType. Thus we need to adjust two things for DisplayOrientedMeteringPointFactory.
 * (1) Calculate the new width/height of factory based on the FIT/FULL scaleType to make it
 * represent the full preview
 * (2) Add offset to the (x, y) in convertPoint based on the BEGIN/CENTER/END scaleType.
 */
class PreviewViewMeteringPointFactory extends MeteringPointFactory {
    @GuardedBy("mLock")
    private DisplayOrientedMeteringPointFactory mDisplayOrientedMeteringPointFactory;

    @GuardedBy("mLock")
    private float mViewWidth;

    @GuardedBy("mLock")
    private float mViewHeight;

    @GuardedBy("mLock")
    private float mFactoryWidth;

    @GuardedBy("mLock")
    private float mFactoryHeight;

    @Nullable
    @GuardedBy("mLock")
    private Size mResolution;

    @GuardedBy("mLock")
    private Display mDisplay;

    @GuardedBy("mLock")
    private CameraInfo mCameraInfo;

    @GuardedBy("mLock")
    private ScaleType mScaleType = ScaleType.FILL_CENTER;

    @GuardedBy("mLock")
    private boolean mIsValid;

    @GuardedBy("mLock")
    private boolean mIsCalculationStale = true;

    // Synchronize access to all the parameters since they can be updated by the main thread at
    // any time due to layout changes while the CameraInfo does not have a guaranteed thread it
    // will be called on.
    // In addition the metering point factory convert can be called on any thread.
    private final Object mLock = new Object();

    // TODO(b/150916047): Use Previewview scaleType transform to implement instead.
    PreviewViewMeteringPointFactory() {
        mIsValid = false;
    }

    /**
     * Initialize metering point factory with parameters.
     *
     * @param display the display on which the {@link PreviewView} is display
     * @param cameraInfo the information of the {@link Camera} that the PreviewView is attached to
     * @param resolution the resolution of the {@link PreviewViewImplementation} that is used by
     *                   the PreviewView
     * @param scaleType the scale type of the PreviewView
     * @param viewWidth the width of the PreviewView
     * @param viewHeight the height of the PreviewView
     */
    PreviewViewMeteringPointFactory(@Nullable Display display,
            @Nullable CameraInfo cameraInfo, @Nullable Size resolution,
            @Nullable ScaleType scaleType, int viewWidth, int viewHeight) {
        mDisplay = display;
        mCameraInfo = cameraInfo;
        mResolution = resolution;
        mScaleType = scaleType;
        mViewWidth = viewWidth;
        mViewHeight = viewHeight;
        recalculate();
    }

    void recalculate() {
        synchronized (mLock) {
            mIsCalculationStale = false;

            // invalid condition
            if (mResolution == null
                    || mViewWidth <= 0
                    || mViewHeight <= 0
                    || mDisplay == null
                    || mCameraInfo == null) {
                mIsValid = false;
                return;
            }
            mIsValid = true;

            boolean needReverse = false;

            if (isNaturalPortrait(mDisplay)) {
                if (mDisplay.getRotation() == Surface.ROTATION_0
                        || mDisplay.getRotation() == Surface.ROTATION_180) {
                    needReverse = true;
                }
            } else {
                if (mDisplay.getRotation() == Surface.ROTATION_90
                        || mDisplay.getRotation() == Surface.ROTATION_270) {
                    needReverse = true;
                }
            }

            int bufferRotatedWidth;
            int bufferRotatedHeight;
            if (needReverse) {
                bufferRotatedWidth = mResolution.getHeight();
                bufferRotatedHeight = mResolution.getWidth();
            } else {
                bufferRotatedWidth = mResolution.getWidth();
                bufferRotatedHeight = mResolution.getHeight();
            }

            final float scale;
            if (mScaleType == ScaleType.FILL_CENTER
                    || mScaleType == ScaleType.FILL_START
                    || mScaleType == ScaleType.FILL_END) {
                scale = Math.max((float) mViewWidth / bufferRotatedWidth,
                        (float) mViewHeight / bufferRotatedHeight);
            } else if (mScaleType == ScaleType.FIT_START
                    || mScaleType == ScaleType.FIT_CENTER
                    || mScaleType == ScaleType.FIT_END) {
                scale = Math.min((float) mViewWidth / bufferRotatedWidth,
                        (float) mViewHeight / bufferRotatedHeight);
            } else {
                throw new IllegalArgumentException("Unknown scale type " + mScaleType);
            }
            mFactoryWidth = bufferRotatedWidth * scale;
            mFactoryHeight = bufferRotatedHeight * scale;
            mDisplayOrientedMeteringPointFactory =
                    new DisplayOrientedMeteringPointFactory(mDisplay, mCameraInfo, mFactoryWidth,
                            mFactoryHeight);
        }
    }

    void setScaleType(@Nullable ScaleType scaleType) {
        synchronized (mLock) {
            if (mScaleType == null || mScaleType != scaleType) {
                mScaleType = scaleType;
                mIsCalculationStale = true;
            }
        }
    }

    void setViewSize(int viewWidth, int viewHeight) {
        synchronized (mLock) {
            if (mViewWidth != viewWidth || mViewHeight != viewHeight) {
                mViewWidth = viewWidth;
                mViewHeight = viewHeight;
                mIsCalculationStale = true;
            }
        }
    }

    void setViewImplementationResolution(@Nullable Size resolution) {
        synchronized (mLock) {
            if (mResolution == null || !mResolution.equals(resolution)) {
                mResolution = resolution;
                mIsCalculationStale = true;
            }
        }
    }

    void setDisplay(@Nullable Display display) {
        synchronized (mLock) {
            if (mDisplay == null || mDisplay != display) {
                mDisplay = display;
                mIsCalculationStale = true;
            }
        }
    }

    void setCameraInfo(@Nullable CameraInfo cameraInfo) {
        synchronized (mLock) {
            if (mCameraInfo == null || mCameraInfo != cameraInfo) {
                mCameraInfo = cameraInfo;
                mIsCalculationStale = true;
            }
        }
    }

    @NonNull
    @Override
    protected PointF convertPoint(float x, float y) {
        synchronized (mLock) {
            if (mIsCalculationStale) {
                recalculate();
            }

            if (!mIsValid) {
                // Returns a invalid point whose value is out of range [0..1]
                return new PointF(2.0f, 2.0f);
            }
            float offsetX = 0f;
            float offsetY = 0f;

            if (mScaleType == ScaleType.FILL_START
                    || mScaleType == ScaleType.FIT_START) {
                offsetX = 0;
                offsetY = 0;
            } else if (mScaleType == ScaleType.FILL_CENTER
                    || mScaleType == ScaleType.FIT_CENTER) {
                offsetX = (mFactoryWidth - mViewWidth) / 2;
                offsetY = (mFactoryHeight - mViewHeight) / 2;
            } else if (mScaleType == ScaleType.FILL_END
                    || mScaleType == ScaleType.FIT_END) {
                offsetX = (mFactoryWidth - mViewWidth);
                offsetY = (mFactoryHeight - mViewHeight);
            }

            float adjustedX = x + offsetX;
            float adjustedY = y + offsetY;

            // DisplayOrientedMeteringPointFactory#convertPoint is not public, thus we cannot use
            // it to convert the point. A alternative approach is using createPoint() to create a
            // MeteringPoint and get X, Y from it.
            MeteringPoint pt = mDisplayOrientedMeteringPointFactory.createPoint(adjustedX,
                    adjustedY);
            return new PointF(pt.getX(), pt.getY());
        }
    }

    private boolean isNaturalPortrait(Display display) {
        final Point deviceSize = new Point();
        display.getRealSize(deviceSize);
        int rotation = display.getRotation();

        final int width = deviceSize.x;
        final int height = deviceSize.y;
        return ((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180)
                && width < height) || (
                (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270)
                        && width >= height);
    }
}