1 /*
2  * Copyright (C) 2017 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 androidx.appcompat.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.ContextWrapper;
24 import android.content.res.Resources;
25 import android.graphics.PixelFormat;
26 import android.graphics.Rect;
27 import android.util.DisplayMetrics;
28 import android.util.Log;
29 import android.view.Gravity;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.WindowManager;
34 import android.widget.TextView;
35 
36 import androidx.annotation.RestrictTo;
37 import androidx.appcompat.R;
38 
39 /**
40  * A popup window displaying a text message aligned to a specified view.
41  *
42  * @hide
43  */
44 @RestrictTo(LIBRARY_GROUP)
45 class TooltipPopup {
46     private static final String TAG = "TooltipPopup";
47 
48     private final Context mContext;
49 
50     private final View mContentView;
51     private final TextView mMessageView;
52 
53     private final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
54     private final Rect mTmpDisplayFrame = new Rect();
55     private final int[] mTmpAnchorPos = new int[2];
56     private final int[] mTmpAppPos = new int[2];
57 
TooltipPopup(Context context)58     TooltipPopup(Context context) {
59         mContext = context;
60 
61         mContentView = LayoutInflater.from(mContext).inflate(R.layout.abc_tooltip, null);
62         mMessageView = (TextView) mContentView.findViewById(R.id.message);
63 
64         mLayoutParams.setTitle(getClass().getSimpleName());
65         mLayoutParams.packageName = mContext.getPackageName();
66         mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
67         mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
68         mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
69         mLayoutParams.format = PixelFormat.TRANSLUCENT;
70         mLayoutParams.windowAnimations = R.style.Animation_AppCompat_Tooltip;
71         mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
72                 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
73     }
74 
show(View anchorView, int anchorX, int anchorY, boolean fromTouch, CharSequence tooltipText)75     void show(View anchorView, int anchorX, int anchorY, boolean fromTouch,
76             CharSequence tooltipText) {
77         if (isShowing()) {
78             hide();
79         }
80 
81         mMessageView.setText(tooltipText);
82 
83         computePosition(anchorView, anchorX, anchorY, fromTouch, mLayoutParams);
84 
85         WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
86         wm.addView(mContentView, mLayoutParams);
87     }
88 
hide()89     void hide() {
90         if (!isShowing()) {
91             return;
92         }
93 
94         WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
95         wm.removeView(mContentView);
96     }
97 
isShowing()98     boolean isShowing() {
99         return mContentView.getParent() != null;
100     }
101 
computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch, WindowManager.LayoutParams outParams)102     private void computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch,
103             WindowManager.LayoutParams outParams) {
104         outParams.token = anchorView.getApplicationWindowToken();
105         final int tooltipPreciseAnchorThreshold = mContext.getResources().getDimensionPixelOffset(
106                 R.dimen.tooltip_precise_anchor_threshold);
107 
108         final int offsetX;
109         if (anchorView.getWidth() >= tooltipPreciseAnchorThreshold) {
110             // Wide view. Align the tooltip horizontally to the precise X position.
111             offsetX = anchorX;
112         } else {
113             // Otherwise anchor the tooltip to the view center.
114             offsetX = anchorView.getWidth() / 2;  // Center on the view horizontally.
115         }
116 
117         final int offsetBelow;
118         final int offsetAbove;
119         if (anchorView.getHeight() >= tooltipPreciseAnchorThreshold) {
120             // Tall view. Align the tooltip vertically to the precise Y position.
121             final int offsetExtra = mContext.getResources().getDimensionPixelOffset(
122                     R.dimen.tooltip_precise_anchor_extra_offset);
123             offsetBelow = anchorY + offsetExtra;
124             offsetAbove = anchorY - offsetExtra;
125         } else {
126             // Otherwise anchor the tooltip to the view center.
127             offsetBelow = anchorView.getHeight();  // Place below the view in most cases.
128             offsetAbove = 0;  // Place above the view if the tooltip does not fit below.
129         }
130 
131         outParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
132 
133         final int tooltipOffset = mContext.getResources().getDimensionPixelOffset(
134                 fromTouch ? R.dimen.tooltip_y_offset_touch : R.dimen.tooltip_y_offset_non_touch);
135 
136         final View appView = getAppRootView(anchorView);
137         if (appView == null) {
138             Log.e(TAG, "Cannot find app view");
139             return;
140         }
141         appView.getWindowVisibleDisplayFrame(mTmpDisplayFrame);
142         if (mTmpDisplayFrame.left < 0 && mTmpDisplayFrame.top < 0) {
143             // No meaningful display frame, the anchor view is probably in a subpanel
144             // (such as a popup window). Use the screen frame as a reasonable approximation.
145             final Resources res = mContext.getResources();
146             final int statusBarHeight;
147             int resourceId = res.getIdentifier("status_bar_height", "dimen", "android");
148             if (resourceId != 0) {
149                 statusBarHeight = res.getDimensionPixelSize(resourceId);
150             } else {
151                 statusBarHeight = 0;
152             }
153             final DisplayMetrics metrics = res.getDisplayMetrics();
154             mTmpDisplayFrame.set(0, statusBarHeight, metrics.widthPixels, metrics.heightPixels);
155         }
156         appView.getLocationOnScreen(mTmpAppPos);
157 
158         anchorView.getLocationOnScreen(mTmpAnchorPos);
159         mTmpAnchorPos[0] -= mTmpAppPos[0];
160         mTmpAnchorPos[1] -= mTmpAppPos[1];
161         // mTmpAnchorPos is now relative to the main app window.
162 
163         outParams.x = mTmpAnchorPos[0] + offsetX - appView.getWidth() / 2;
164 
165         final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
166         mContentView.measure(spec, spec);
167         final int tooltipHeight = mContentView.getMeasuredHeight();
168 
169         final int yAbove = mTmpAnchorPos[1] + offsetAbove - tooltipOffset - tooltipHeight;
170         final int yBelow = mTmpAnchorPos[1] + offsetBelow + tooltipOffset;
171         if (fromTouch) {
172             if (yAbove >= 0) {
173                 outParams.y = yAbove;
174             } else {
175                 outParams.y = yBelow;
176             }
177         } else {
178             if (yBelow + tooltipHeight <= mTmpDisplayFrame.height()) {
179                 outParams.y = yBelow;
180             } else {
181                 outParams.y = yAbove;
182             }
183         }
184     }
185 
getAppRootView(View anchorView)186     private static View getAppRootView(View anchorView) {
187         View rootView = anchorView.getRootView();
188         ViewGroup.LayoutParams lp = rootView.getLayoutParams();
189         if (lp instanceof WindowManager.LayoutParams
190                 && (((WindowManager.LayoutParams) lp).type
191                     == WindowManager.LayoutParams.TYPE_APPLICATION)) {
192             // This covers regular app windows and Dialog windows.
193             return rootView;
194         }
195         // For non-application window types (such as popup windows) try to find the main app window
196         // through the context.
197         Context context = anchorView.getContext();
198         while (context instanceof ContextWrapper) {
199             if (context instanceof Activity) {
200                 return ((Activity) context).getWindow().getDecorView();
201             } else {
202                 context = ((ContextWrapper) context).getBaseContext();
203             }
204         }
205         // Main app window not found, fall back to the anchor's root view. There is no guarantee
206         // that the tooltip position will be computed correctly.
207         return rootView;
208     }
209 }
210