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