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