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