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