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