• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.android.systemui.tv.media.settings;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.graphics.drawable.Drawable;
22 import android.os.CountDownTimer;
23 import android.util.AttributeSet;
24 import android.util.Log;
25 import android.view.Gravity;
26 import android.view.KeyEvent;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.view.ViewTreeObserver;
30 import android.widget.FrameLayout;
31 import android.widget.ImageView;
32 import android.widget.PopupWindow;
33 import android.widget.TextView;
34 
35 import androidx.annotation.Nullable;
36 
37 import java.util.Objects;
38 
39 import com.android.systemui.tv.res.R;
40 
41 /**
42  * Base control widget that has default tooltip functionality.
43  **/
44 public class ControlWidget extends FrameLayout
45         implements View.OnFocusChangeListener, View.OnKeyListener {
46     private static final String TAG = ControlWidget.class.getSimpleName();
47     private static final boolean DEBUG = false;
48 
49     private static final int TOOLTIP_DELAY_MS = 2000;
50     private TooltipConfig mTooltipConfig;
51     private View mTooltipView;
52     private PopupWindow mTooltipWindow;
53     @Nullable
54     private OnKeyListener mExternalOnKeyListener;
55     @Nullable
56     private OnFocusChangeListener mExternalOnFocusChangeListener;
57 
58     private final CountDownTimer mTooltipTimer;
59 
ControlWidget(Context context)60     public ControlWidget(Context context) {
61         this(context, /* attrs= */ null);
62     }
63 
ControlWidget(Context context, @Nullable AttributeSet attrs)64     public ControlWidget(Context context, @Nullable AttributeSet attrs) {
65         this(context, attrs, /* defStyleAttr= */ 0);
66     }
67 
68     @SuppressWarnings("nullness")
ControlWidget(Context context, @Nullable AttributeSet attrs, int defStyleAttr)69     public ControlWidget(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
70         super(context, attrs, defStyleAttr);
71         super.setOnFocusChangeListener(this);
72         super.setOnKeyListener(this);
73         this.mTooltipTimer = createTooltipTimer(TOOLTIP_DELAY_MS);
74     }
75 
76     @Override
setEnabled(boolean enabled)77     public void setEnabled(boolean enabled) {
78         if (this.isEnabled() == enabled) {
79             return;
80         }
81         super.setEnabled(enabled);
82         setAlpha(enabled ? 1f : 0.6f);
83     }
84 
85     @Override
setOnKeyListener(@ullable OnKeyListener onKeyListener)86     public void setOnKeyListener(@Nullable OnKeyListener onKeyListener) {
87         this.mExternalOnKeyListener = onKeyListener;
88     }
89 
90     @Override
setOnFocusChangeListener(@ullable OnFocusChangeListener onFocusChangeListener)91     public void setOnFocusChangeListener(@Nullable OnFocusChangeListener onFocusChangeListener) {
92         this.mExternalOnFocusChangeListener = onFocusChangeListener;
93     }
94 
setTooltipConfig(TooltipConfig tooltipConfig)95     public void setTooltipConfig(TooltipConfig tooltipConfig) {
96         if (Objects.equals(this.mTooltipConfig, tooltipConfig)) {
97             return;
98         }
99 
100         this.mTooltipConfig = tooltipConfig;
101 
102         if (tooltipConfig == null) {
103             dismissTooltipWindow();
104         } else {
105             // reflect tool tip config changes
106             if (!tooltipConfig.getShouldShowTooltip()) {
107                 dismissTooltipWindow();
108             }
109 
110             if (mTooltipView != null && mTooltipWindow != null && mTooltipWindow.isShowing()) {
111                 loadText();
112                 loadSummary();
113                 loadImage();
114             }
115         }
116     }
117 
shouldAbortShowingTooltip()118     private boolean shouldAbortShowingTooltip() {
119         return !isFocused() || mTooltipConfig == null || !mTooltipConfig.getShouldShowTooltip()
120                 || !isAttachedToWindow();
121     }
122 
showTooltipView()123     private void showTooltipView() {
124         if (shouldAbortShowingTooltip()) {
125             return;
126         }
127         int width = getContext().getResources().getDimensionPixelSize(R.dimen.tooltip_window_width);
128 
129         // Construct tooltip pop-up window.
130         mTooltipView = View.inflate(this.getContext(), R.layout.tooltip_window, null);
131         mTooltipView.getViewTreeObserver().addOnGlobalLayoutListener(
132                 new ViewTreeObserver.OnGlobalLayoutListener() {
133 
134                     @Override
135                     public void onGlobalLayout() {
136                         if (DEBUG) Log.d(TAG, "onGlobalLayoutListener");
137                         mTooltipView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
138                         if (shouldAbortShowingTooltip()) {
139                             return;
140                         }
141                         // Calculate tooltip location on screen.
142                         Rect location = locateView(ControlWidget.this);
143                         int[] position = ControlWidget.this.calculateWindowOffset(location);
144                         if (DEBUG) {
145                             Log.d(TAG,
146                                     "new position, x=" + position[0] + ", y=" + position[1]);
147                         }
148 
149                         // Only update the position, not the size
150                         mTooltipWindow.update(position[0], position[1], -1, -1);
151                         mTooltipView.postDelayed(() -> {
152                             if (shouldAbortShowingTooltip()) {
153                                 return;
154                             }
155                             if (DEBUG) Log.d(TAG, "postDelayed, make visible");
156                             mTooltipView.setVisibility(VISIBLE);
157                         }, 100);
158                     }
159                 });
160 
161         mTooltipWindow = new PopupWindow(mTooltipView, width, ViewGroup.LayoutParams.WRAP_CONTENT,
162                 false);
163         mTooltipWindow.setAnimationStyle(R.style.ControlWidgetTooltipWindowAnimation);
164         mTooltipView.setVisibility(INVISIBLE);
165 
166         // Load image and text.
167         loadImage();
168         loadSummary();
169         loadText();
170 
171         // Calculate tooltip location on screen.
172         Rect location = locateView(this);
173         int[] position = calculateWindowOffset(location);
174 
175         // Display tooltip window.
176         mTooltipWindow.showAtLocation(this, Gravity.NO_GRAVITY, position[0], position[1]);
177     }
178 
dismissTooltipWindow()179     public void dismissTooltipWindow() {
180         if (mTooltipWindow != null && mTooltipWindow.isShowing()) {
181             mTooltipWindow.dismiss();
182         } else {
183             mTooltipTimer.cancel();
184         }
185     }
186 
locateView(View view)187     private static Rect locateView(View view) {
188         int[] locationInt = new int[2];
189         view.getLocationOnScreen(locationInt);
190         Rect location = new Rect();
191         location.left = locationInt[0];
192         location.top = locationInt[1];
193         location.right = location.left + view.getWidth();
194         location.bottom = location.top + view.getHeight();
195         return location;
196     }
197 
calculateWindowOffset(Rect focusedRect)198     private int[] calculateWindowOffset(Rect focusedRect) {
199         int[] windowOffset = new int[2];
200         int tooltipWidth = getContext().getResources().getDimensionPixelSize(
201                 R.dimen.tooltip_window_width);
202         boolean isRtl =
203                 getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
204         // X offset
205         if (isRtl) {
206             // controlWidget.width  + margin
207             windowOffset[0] = focusedRect.right
208                     + getContext().getResources()
209                     .getDimensionPixelOffset(R.dimen.tooltip_window_horizontal_margin);
210         } else {
211             windowOffset[0] = -tooltipWidth
212                     - getContext().getResources()
213                     .getDimensionPixelOffset(R.dimen.tooltip_window_horizontal_margin);
214         }
215         // Y offset
216         if (mTooltipView.getMeasuredHeight() <= 0) {
217             // Height unknown -> fixed offset
218             windowOffset[1] = focusedRect.top
219                     - getContext().getResources()
220                     .getDimensionPixelOffset(R.dimen.media_dialog_margin_vertical)
221                     + getContext().getResources().getDimensionPixelSize(
222                     R.dimen.tooltip_window_vertical_margin);
223         } else {
224             // Height known -> calculate centered position
225             windowOffset[1] = (focusedRect.top + focusedRect.bottom) / 2
226                     - mTooltipView.getMeasuredHeight() / 2
227                     - getContext().getResources()
228                     .getDimensionPixelOffset(R.dimen.media_dialog_margin_vertical);
229         }
230 
231         return windowOffset;
232     }
233 
loadText()234     private void loadText() {
235         CharSequence text = mTooltipConfig.getTooltipText();
236         TextView textView = mTooltipView.requireViewById(R.id.tooltip_text);
237 
238         if (text == null || text.isEmpty()) {
239             textView.setVisibility(GONE);
240         } else {
241             textView.setVisibility(VISIBLE);
242             textView.setText(text);
243         }
244     }
245 
loadSummary()246     private void loadSummary() {
247         CharSequence summary = mTooltipConfig.getTooltipSummary();
248         TextView summaryView = mTooltipView.requireViewById(R.id.tooltip_summary);
249 
250         if (summary == null || summary.isEmpty()) {
251             summaryView.setVisibility(GONE);
252         } else {
253             summaryView.setVisibility(VISIBLE);
254             summaryView.setText(summary);
255         }
256     }
257 
loadImage()258     private void loadImage() {
259         ImageView tooltipImage = mTooltipView.requireViewById(R.id.tooltip_image);
260         Drawable imageDrawable = mTooltipConfig.getImageDrawable();
261         if (imageDrawable == null) {
262             tooltipImage.setVisibility(GONE);
263         } else {
264             tooltipImage.setImageDrawable(imageDrawable);
265             tooltipImage.setVisibility(VISIBLE);
266         }
267     }
268 
createTooltipTimer(long delayMs)269     private CountDownTimer createTooltipTimer(long delayMs) {
270         return new CountDownTimer(delayMs, delayMs) {
271             @Override
272             public void onTick(long millisUntilFinished) {
273             }
274 
275             @Override
276             public void onFinish() {
277                 showTooltipView();
278             }
279         };
280     }
281 
282     @Override
283     public void onFocusChange(View v, boolean hasFocus) {
284         if (mExternalOnFocusChangeListener != null) {
285             mExternalOnFocusChangeListener.onFocusChange(v, hasFocus);
286         }
287         if (mTooltipConfig == null || !mTooltipConfig.getShouldShowTooltip()) {
288             return;
289         }
290         if (hasFocus) {
291             mTooltipTimer.start();
292         } else {
293             if (mTooltipWindow != null && mTooltipWindow.isShowing()) {
294                 mTooltipWindow.dismiss();
295             } else {
296                 mTooltipTimer.cancel();
297             }
298         }
299     }
300 
301     @Override
302     public boolean onKey(View v, int keyCode, KeyEvent event) {
303         if (event.getAction() == KeyEvent.ACTION_DOWN) {
304             if (mTooltipWindow != null && mTooltipWindow.isShowing()) {
305                 mTooltipWindow.dismiss();
306             } else {
307                 mTooltipTimer.cancel();
308             }
309             if (mTooltipConfig != null
310                     && mTooltipConfig.getShouldShowTooltip()
311                     && isFocused()) {
312                 // Start the timer in case user hover 3 seconds again.
313                 mTooltipTimer.start();
314             }
315         }
316         if (mExternalOnKeyListener != null) {
317             return mExternalOnKeyListener.onKey(v, keyCode, event);
318         }
319         return false;
320     }
321 
322     /** Wrapper class for callers to set tool tip related attributes. */
323     public static final class TooltipConfig {
324         private boolean mShouldShowTooltip;
325         private Drawable mImageDrawable;
326         private CharSequence mTooltipText;
327         private CharSequence mTooltipSummary;
328 
329         public void setShouldShowTooltip(boolean shouldShowTooltip) {
330             this.mShouldShowTooltip = shouldShowTooltip;
331         }
332 
333         public boolean getShouldShowTooltip() {
334             return mShouldShowTooltip;
335         }
336 
337         public void setImageDrawable(Drawable imageDrawable) {
338             this.mImageDrawable = imageDrawable;
339         }
340 
341         public Drawable getImageDrawable() {
342             return mImageDrawable;
343         }
344 
345         public void setTooltipText(CharSequence tooltipText) {
346             this.mTooltipText = tooltipText;
347         }
348 
349         public CharSequence getTooltipText() {
350             return mTooltipText;
351         }
352 
353         public void setTooltipSummary(CharSequence tooltipSummary) {
354             this.mTooltipSummary = tooltipSummary;
355         }
356 
357         public CharSequence getTooltipSummary() {
358             return mTooltipSummary;
359         }
360     }
361 }
362