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}