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