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 
17 package com.android.internal.view;
18 
19 import android.content.Context;
20 import android.graphics.PixelFormat;
21 import android.graphics.Rect;
22 import android.util.Slog;
23 import android.view.Gravity;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.WindowManager;
27 import android.view.WindowManagerGlobal;
28 import android.widget.TextView;
29 
30 public class TooltipPopup {
31     private static final String TAG = "TooltipPopup";
32 
33     private final Context mContext;
34 
35     private final View mContentView;
36     private final TextView mMessageView;
37 
38     private final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams();
39     private final Rect mTmpDisplayFrame = new Rect();
40     private final int[] mTmpAnchorPos = new int[2];
41     private final int[] mTmpAppPos = new int[2];
42 
TooltipPopup(Context context)43     public TooltipPopup(Context context) {
44         mContext = context;
45 
46         mContentView = LayoutInflater.from(mContext).inflate(
47                 com.android.internal.R.layout.tooltip, null);
48         mMessageView = (TextView) mContentView.findViewById(
49                 com.android.internal.R.id.message);
50 
51         mLayoutParams.setTitle(
52                 mContext.getString(com.android.internal.R.string.tooltip_popup_title));
53         mLayoutParams.packageName = mContext.getOpPackageName();
54         mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL;
55         mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
56         mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
57         mLayoutParams.format = PixelFormat.TRANSLUCENT;
58         mLayoutParams.windowAnimations = com.android.internal.R.style.Animation_Tooltip;
59         mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
60                 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
61     }
62 
show(View anchorView, int anchorX, int anchorY, boolean fromTouch, CharSequence tooltipText)63     public void show(View anchorView, int anchorX, int anchorY, boolean fromTouch,
64             CharSequence tooltipText) {
65         if (isShowing()) {
66             hide();
67         }
68 
69         mMessageView.setText(tooltipText);
70 
71         computePosition(anchorView, anchorX, anchorY, fromTouch, mLayoutParams);
72 
73         WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
74         wm.addView(mContentView, mLayoutParams);
75     }
76 
hide()77     public void hide() {
78         if (!isShowing()) {
79             return;
80         }
81 
82         WindowManager wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
83         wm.removeView(mContentView);
84     }
85 
getContentView()86     public View getContentView() {
87         return mContentView;
88     }
89 
isShowing()90     public boolean isShowing() {
91         return mContentView.getParent() != null;
92     }
93 
computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch, WindowManager.LayoutParams outParams)94     private void computePosition(View anchorView, int anchorX, int anchorY, boolean fromTouch,
95             WindowManager.LayoutParams outParams) {
96         outParams.token = anchorView.getApplicationWindowToken();
97 
98         final int tooltipPreciseAnchorThreshold = mContext.getResources().getDimensionPixelOffset(
99                 com.android.internal.R.dimen.tooltip_precise_anchor_threshold);
100 
101         final int offsetX;
102         if (anchorView.getWidth() >= tooltipPreciseAnchorThreshold) {
103             // Wide view. Align the tooltip horizontally to the precise X position.
104             offsetX = anchorX;
105         } else {
106             // Otherwise anchor the tooltip to the view center.
107             offsetX = anchorView.getWidth() / 2;  // Center on the view horizontally.
108         }
109 
110         final int offsetBelow;
111         final int offsetAbove;
112         if (anchorView.getHeight() >= tooltipPreciseAnchorThreshold) {
113             // Tall view. Align the tooltip vertically to the precise Y position.
114             final int offsetExtra = mContext.getResources().getDimensionPixelOffset(
115                     com.android.internal.R.dimen.tooltip_precise_anchor_extra_offset);
116             offsetBelow = anchorY + offsetExtra;
117             offsetAbove = anchorY - offsetExtra;
118         } else {
119             // Otherwise anchor the tooltip to the view center.
120             offsetBelow = anchorView.getHeight();  // Place below the view in most cases.
121             offsetAbove = 0;  // Place above the view if the tooltip does not fit below.
122         }
123 
124         outParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
125 
126         final int tooltipOffset = mContext.getResources().getDimensionPixelOffset(
127                 fromTouch ? com.android.internal.R.dimen.tooltip_y_offset_touch
128                         : com.android.internal.R.dimen.tooltip_y_offset_non_touch);
129 
130         // Find the main app window. The popup window will be positioned relative to it.
131         final View appView = WindowManagerGlobal.getInstance().getWindowView(
132                 anchorView.getApplicationWindowToken());
133         if (appView == null) {
134             Slog.e(TAG, "Cannot find app view");
135             return;
136         }
137         appView.getWindowVisibleDisplayFrame(mTmpDisplayFrame);
138         appView.getLocationOnScreen(mTmpAppPos);
139 
140         anchorView.getLocationOnScreen(mTmpAnchorPos);
141         mTmpAnchorPos[0] -= mTmpAppPos[0];
142         mTmpAnchorPos[1] -= mTmpAppPos[1];
143         // mTmpAnchorPos is now relative to the main app window.
144 
145         outParams.x = mTmpAnchorPos[0] + offsetX - appView.getWidth() / 2;
146 
147         final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
148         mContentView.measure(spec, spec);
149         final int tooltipHeight = mContentView.getMeasuredHeight();
150 
151         final int yAbove = mTmpAnchorPos[1] + offsetAbove - tooltipOffset - tooltipHeight;
152         final int yBelow = mTmpAnchorPos[1] + offsetBelow + tooltipOffset;
153         if (fromTouch) {
154             if (yAbove >= 0) {
155                 outParams.y = yAbove;
156             } else {
157                 outParams.y = yBelow;
158             }
159         } else {
160             // Use mTmpDisplayFrame.height() as the lower boundary instead of appView.getHeight(),
161             // as the latter includes the navigation bar, and tooltips do not look good over
162             // the navigation bar.
163             if (yBelow + tooltipHeight <= mTmpDisplayFrame.height()) {
164                 outParams.y = yBelow;
165             } else {
166                 outParams.y = yAbove;
167             }
168         }
169     }
170 }
171