1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.example.rscamera;
17
18import android.content.Context;
19import android.content.res.TypedArray;
20import android.util.AttributeSet;
21import android.util.Log;
22import android.view.GestureDetector;
23import android.view.MotionEvent;
24import android.view.SurfaceView;
25import android.view.View;
26import android.view.ViewGroup.LayoutParams;
27
28import com.android.example.rscamera.rscamera.R;
29
30/**
31 * A SurfaceView that maintains its aspect ratio to be a desired target value.
32 * <p/>
33 * <p>Depending on the layout, the FixedAspectSurfaceView may not be able to maintain the
34 * requested aspect ratio. This can happen if both the width and the height are exactly
35 * determined by the layout.  To avoid this, ensure that either the height or the width is
36 * adjustable by the view; for example, by setting the layout parameters to be WRAP_CONTENT for
37 * the dimension that is best adjusted to maintain the aspect ratio.</p>
38 */
39public class FixedAspectSurfaceView extends SurfaceView {
40
41    /**
42     * Desired width/height ratio
43     */
44    private float mAspectRatio;
45
46    private GestureDetector mGestureDetector;
47
48    public FixedAspectSurfaceView(Context context, AttributeSet attrs) {
49        super(context, attrs);
50
51        // Get initial aspect ratio from custom attributes
52        TypedArray a =
53                context.getTheme().obtainStyledAttributes(attrs,
54                        R.styleable.FixedAspectSurfaceView, 0, 0);
55        setAspectRatio(a.getFloat(
56                R.styleable.FixedAspectSurfaceView_aspectRatio, 1.f));
57        a.recycle();
58    }
59
60    /**
61     * Set the desired aspect ratio for this view.
62     *
63     * @param aspect the desired width/height ratio in the current UI orientation. Must be a
64     *               positive value.
65     */
66    public void setAspectRatio(float aspect) {
67        if (aspect <= 0) {
68            throw new IllegalArgumentException("Aspect ratio must be positive");
69        }
70        mAspectRatio = aspect;
71        requestLayout();
72    }
73
74    /**
75     * Set a gesture listener to listen for touch events
76     */
77    public void setGestureListener(Context context, GestureDetector.OnGestureListener listener) {
78        if (listener == null) {
79            mGestureDetector = null;
80        } else {
81            mGestureDetector = new GestureDetector(context, listener);
82        }
83    }
84
85    @Override
86    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
87
88        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
89        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
90        int width = MeasureSpec.getSize(widthMeasureSpec);
91        int height = MeasureSpec.getSize(heightMeasureSpec);
92
93        // General goal: Adjust dimensions to maintain the requested aspect ratio as much
94        // as possible. Depending on the measure specs handed down, this may not be possible
95
96        // Only set one of these to true
97        boolean scaleWidth = false;
98        boolean scaleHeight = false;
99
100        // Sort out which dimension to scale, if either can be. There are 9 combinations of
101        // possible measure specs; a few cases below handle multiple combinations
102        if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
103            // Can't adjust sizes at all, do nothing
104        } else if (widthMode == MeasureSpec.EXACTLY) {
105            // Width is fixed, heightMode either AT_MOST or UNSPECIFIED, so adjust height
106            scaleHeight = true;
107        } else if (heightMode == MeasureSpec.EXACTLY) {
108            // Height is fixed, widthMode either AT_MOST or UNSPECIFIED, so adjust width
109            scaleWidth = true;
110        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
111            // Need to fit into box <= [width, height] in size.
112            // Maximize the View's area while maintaining aspect ratio
113            // This means keeping one dimension as large as possible and shrinking the other
114            float boxAspectRatio = width / (float) height;
115            if (boxAspectRatio > mAspectRatio) {
116                // Box is wider than requested aspect; pillarbox
117                scaleWidth = true;
118            } else {
119                // Box is narrower than requested aspect; letterbox
120                scaleHeight = true;
121            }
122        } else if (widthMode == MeasureSpec.AT_MOST) {
123            // Maximize width, heightSpec is UNSPECIFIED
124            scaleHeight = true;
125        } else if (heightMode == MeasureSpec.AT_MOST) {
126            // Maximize height, widthSpec is UNSPECIFIED
127            scaleWidth = true;
128        } else {
129            // Both MeasureSpecs are UNSPECIFIED. This is probably a pathological layout,
130            // with width == height == 0
131            // but arbitrarily scale height anyway
132            scaleHeight = true;
133        }
134
135        // Do the scaling
136        if (scaleWidth) {
137            width = (int) (height * mAspectRatio);
138        } else if (scaleHeight) {
139            height = (int) (width / mAspectRatio);
140        }
141
142        // Override width/height if needed for EXACTLY and AT_MOST specs
143        width = View.resolveSizeAndState(width, widthMeasureSpec, 0);
144        height = View.resolveSizeAndState(height, heightMeasureSpec, 0);
145
146        // Finally set the calculated dimensions
147        setMeasuredDimension(width, height);
148    }
149
150    @Override
151    public boolean onTouchEvent(MotionEvent event) {
152        if (mGestureDetector != null) {
153            return mGestureDetector.onTouchEvent(event);
154        }
155        return false;
156    }
157}