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