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