1 /* 2 * Copyright (C) 2016 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 package com.google.android.exoplayer2.ui; 17 18 import android.content.Context; 19 import android.content.res.TypedArray; 20 import android.util.AttributeSet; 21 import android.widget.FrameLayout; 22 import androidx.annotation.IntDef; 23 import androidx.annotation.Nullable; 24 import java.lang.annotation.Documented; 25 import java.lang.annotation.Retention; 26 import java.lang.annotation.RetentionPolicy; 27 28 /** 29 * A {@link FrameLayout} that resizes itself to match a specified aspect ratio. 30 */ 31 public final class AspectRatioFrameLayout extends FrameLayout { 32 33 /** Listener to be notified about changes of the aspect ratios of this view. */ 34 public interface AspectRatioListener { 35 36 /** 37 * Called when either the target aspect ratio or the view aspect ratio is updated. 38 * 39 * @param targetAspectRatio The aspect ratio that has been set in {@link #setAspectRatio(float)} 40 * @param naturalAspectRatio The natural aspect ratio of this view (before its width and height 41 * are modified to satisfy the target aspect ratio). 42 * @param aspectRatioMismatch Whether the target and natural aspect ratios differ enough for 43 * changing the resize mode to have an effect. 44 */ onAspectRatioUpdated( float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch)45 void onAspectRatioUpdated( 46 float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch); 47 } 48 49 // LINT.IfChange 50 /** 51 * Resize modes for {@link AspectRatioFrameLayout}. One of {@link #RESIZE_MODE_FIT}, {@link 52 * #RESIZE_MODE_FIXED_WIDTH}, {@link #RESIZE_MODE_FIXED_HEIGHT}, {@link #RESIZE_MODE_FILL} or 53 * {@link #RESIZE_MODE_ZOOM}. 54 */ 55 @Documented 56 @Retention(RetentionPolicy.SOURCE) 57 @IntDef({ 58 RESIZE_MODE_FIT, 59 RESIZE_MODE_FIXED_WIDTH, 60 RESIZE_MODE_FIXED_HEIGHT, 61 RESIZE_MODE_FILL, 62 RESIZE_MODE_ZOOM 63 }) 64 public @interface ResizeMode {} 65 66 /** 67 * Either the width or height is decreased to obtain the desired aspect ratio. 68 */ 69 public static final int RESIZE_MODE_FIT = 0; 70 /** 71 * The width is fixed and the height is increased or decreased to obtain the desired aspect ratio. 72 */ 73 public static final int RESIZE_MODE_FIXED_WIDTH = 1; 74 /** 75 * The height is fixed and the width is increased or decreased to obtain the desired aspect ratio. 76 */ 77 public static final int RESIZE_MODE_FIXED_HEIGHT = 2; 78 /** 79 * The specified aspect ratio is ignored. 80 */ 81 public static final int RESIZE_MODE_FILL = 3; 82 /** 83 * Either the width or height is increased to obtain the desired aspect ratio. 84 */ 85 public static final int RESIZE_MODE_ZOOM = 4; 86 // LINT.ThenChange(../../../../../../res/values/attrs.xml) 87 88 /** 89 * The {@link FrameLayout} will not resize itself if the fractional difference between its natural 90 * aspect ratio and the requested aspect ratio falls below this threshold. 91 * 92 * <p>This tolerance allows the view to occupy the whole of the screen when the requested aspect 93 * ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce 94 * the number of view layers that need to be composited by the underlying system, which can help 95 * to reduce power consumption. 96 */ 97 private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f; 98 99 private final AspectRatioUpdateDispatcher aspectRatioUpdateDispatcher; 100 101 @Nullable private AspectRatioListener aspectRatioListener; 102 103 private float videoAspectRatio; 104 @ResizeMode private int resizeMode; 105 AspectRatioFrameLayout(Context context)106 public AspectRatioFrameLayout(Context context) { 107 this(context, /* attrs= */ null); 108 } 109 AspectRatioFrameLayout(Context context, @Nullable AttributeSet attrs)110 public AspectRatioFrameLayout(Context context, @Nullable AttributeSet attrs) { 111 super(context, attrs); 112 resizeMode = RESIZE_MODE_FIT; 113 if (attrs != null) { 114 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, 115 R.styleable.AspectRatioFrameLayout, 0, 0); 116 try { 117 resizeMode = a.getInt(R.styleable.AspectRatioFrameLayout_resize_mode, RESIZE_MODE_FIT); 118 } finally { 119 a.recycle(); 120 } 121 } 122 aspectRatioUpdateDispatcher = new AspectRatioUpdateDispatcher(); 123 } 124 125 /** 126 * Sets the aspect ratio that this view should satisfy. 127 * 128 * @param widthHeightRatio The width to height ratio. 129 */ setAspectRatio(float widthHeightRatio)130 public void setAspectRatio(float widthHeightRatio) { 131 if (this.videoAspectRatio != widthHeightRatio) { 132 this.videoAspectRatio = widthHeightRatio; 133 requestLayout(); 134 } 135 } 136 137 /** 138 * Sets the {@link AspectRatioListener}. 139 * 140 * @param listener The listener to be notified about aspect ratios changes, or null to clear a 141 * listener that was previously set. 142 */ setAspectRatioListener(@ullable AspectRatioListener listener)143 public void setAspectRatioListener(@Nullable AspectRatioListener listener) { 144 this.aspectRatioListener = listener; 145 } 146 147 /** Returns the {@link ResizeMode}. */ getResizeMode()148 public @ResizeMode int getResizeMode() { 149 return resizeMode; 150 } 151 152 /** 153 * Sets the {@link ResizeMode} 154 * 155 * @param resizeMode The {@link ResizeMode}. 156 */ setResizeMode(@esizeMode int resizeMode)157 public void setResizeMode(@ResizeMode int resizeMode) { 158 if (this.resizeMode != resizeMode) { 159 this.resizeMode = resizeMode; 160 requestLayout(); 161 } 162 } 163 164 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)165 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 166 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 167 if (videoAspectRatio <= 0) { 168 // Aspect ratio not set. 169 return; 170 } 171 172 int width = getMeasuredWidth(); 173 int height = getMeasuredHeight(); 174 float viewAspectRatio = (float) width / height; 175 float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; 176 if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) { 177 // We're within the allowed tolerance. 178 aspectRatioUpdateDispatcher.scheduleUpdate(videoAspectRatio, viewAspectRatio, false); 179 return; 180 } 181 182 switch (resizeMode) { 183 case RESIZE_MODE_FIXED_WIDTH: 184 height = (int) (width / videoAspectRatio); 185 break; 186 case RESIZE_MODE_FIXED_HEIGHT: 187 width = (int) (height * videoAspectRatio); 188 break; 189 case RESIZE_MODE_ZOOM: 190 if (aspectDeformation > 0) { 191 width = (int) (height * videoAspectRatio); 192 } else { 193 height = (int) (width / videoAspectRatio); 194 } 195 break; 196 case RESIZE_MODE_FIT: 197 if (aspectDeformation > 0) { 198 height = (int) (width / videoAspectRatio); 199 } else { 200 width = (int) (height * videoAspectRatio); 201 } 202 break; 203 case RESIZE_MODE_FILL: 204 default: 205 // Ignore target aspect ratio 206 break; 207 } 208 aspectRatioUpdateDispatcher.scheduleUpdate(videoAspectRatio, viewAspectRatio, true); 209 super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 210 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 211 } 212 213 /** Dispatches updates to {@link AspectRatioListener}. */ 214 private final class AspectRatioUpdateDispatcher implements Runnable { 215 216 private float targetAspectRatio; 217 private float naturalAspectRatio; 218 private boolean aspectRatioMismatch; 219 private boolean isScheduled; 220 scheduleUpdate( float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch)221 public void scheduleUpdate( 222 float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch) { 223 this.targetAspectRatio = targetAspectRatio; 224 this.naturalAspectRatio = naturalAspectRatio; 225 this.aspectRatioMismatch = aspectRatioMismatch; 226 227 if (!isScheduled) { 228 isScheduled = true; 229 post(this); 230 } 231 } 232 233 @Override run()234 public void run() { 235 isScheduled = false; 236 if (aspectRatioListener == null) { 237 return; 238 } 239 aspectRatioListener.onAspectRatioUpdated( 240 targetAspectRatio, naturalAspectRatio, aspectRatioMismatch); 241 } 242 } 243 } 244