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