OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.widget; |
| 6 |
| 7 import android.content.Context; |
| 8 import android.graphics.Canvas; |
| 9 import android.graphics.ColorFilter; |
| 10 import android.graphics.PixelFormat; |
| 11 import android.graphics.Rect; |
| 12 import android.graphics.drawable.BitmapDrawable; |
| 13 import android.graphics.drawable.Drawable; |
| 14 import android.os.Bundle; |
| 15 import android.view.Gravity; |
| 16 import android.view.View; |
| 17 import android.view.View.MeasureSpec; |
| 18 import android.view.View.OnAttachStateChangeListener; |
| 19 import android.view.View.OnLayoutChangeListener; |
| 20 import android.view.ViewGroup; |
| 21 import android.widget.PopupWindow; |
| 22 import android.widget.TextView; |
| 23 |
| 24 import org.chromium.chrome.R; |
| 25 import org.chromium.ui.base.LocalizationUtils; |
| 26 |
| 27 /** |
| 28 * UI component that handles showing text bubbles. |
| 29 */ |
| 30 public class TextBubble |
| 31 extends PopupWindow implements OnLayoutChangeListener, OnAttachStateChan
geListener { |
| 32 /** Whether to use the intrinsic padding of the bubble background as padding
(boolean). */ |
| 33 public static final String BACKGROUND_INTRINSIC_PADDING = "Background_Intrin
sic_Padding"; |
| 34 |
| 35 /** |
| 36 * Boolean to be used for deciding whether the bubble should be anchored abo
ve or below |
| 37 * the view |
| 38 */ |
| 39 public static final String UP_DOWN = "Up_Down"; |
| 40 |
| 41 /** Style resource Id to be used for text inside the bubble. Should be of ty
pe int. */ |
| 42 public static final String TEXT_STYLE_ID = "Text_Style_Id"; |
| 43 |
| 44 /** Boolean to be used for deciding whether the bubble should be centered to
the view */ |
| 45 public static final String CENTER = "Center"; |
| 46 |
| 47 public static final String ANIM_STYLE_ID = "Animation_Style"; |
| 48 |
| 49 private final int mTooltipEdgeMargin; |
| 50 private final int mTooltipTopMargin; |
| 51 private final int mBubbleTipXMargin; |
| 52 private boolean mAnchorBelow = false; |
| 53 private boolean mCenterView = true; |
| 54 private int mXPosition; |
| 55 private int mYPosition; |
| 56 private View mAnchorView; |
| 57 private final Rect mCachedPaddingRect = new Rect(); |
| 58 |
| 59 // The text view inside the popup containing the tooltip text. |
| 60 private final TextView mTooltipText; |
| 61 |
| 62 /** |
| 63 * Constructor that uses a bundle object to fetch resources and optional boo
lean |
| 64 * values for the {@link TextBubble}. |
| 65 * |
| 66 * Use CENTER for centering the tip to the anchor view. |
| 67 * UP_DOWN for drawing the bubble with tip pointing up or down. |
| 68 * Up is true and Down is false. |
| 69 * LAYOUT_WIDTH_ID Dimension resource Id for the width of the {@link Te
xtView} inside the |
| 70 * bubble. The height is set to half of this value. |
| 71 * |
| 72 * @param context |
| 73 * @param res Bundle object that contains resource ids and optional flags. |
| 74 */ |
| 75 public TextBubble(Context context, Bundle res) { |
| 76 mAnchorBelow = (res.containsKey(UP_DOWN) ? res.getBoolean(UP_DOWN) : tru
e); |
| 77 mCenterView = (res.containsKey(CENTER) ? res.getBoolean(CENTER) : true); |
| 78 mTooltipEdgeMargin = |
| 79 context.getResources().getDimensionPixelSize(R.dimen.tooltip_min
_edge_margin); |
| 80 mTooltipTopMargin = |
| 81 context.getResources().getDimensionPixelSize(R.dimen.tooltip_top
_margin); |
| 82 mBubbleTipXMargin = context.getResources().getDimensionPixelSize(R.dimen
.bubble_tip_margin); |
| 83 |
| 84 setBackgroundDrawable(new BubbleBackgroundDrawable(context, res)); |
| 85 setAnimationStyle(res.containsKey(ANIM_STYLE_ID) ? res.getInt(ANIM_STYLE
_ID) |
| 86 : android.R.style.Anima
tion); |
| 87 |
| 88 mTooltipText = new TextView(context); |
| 89 mTooltipText.setTextAppearance(context, |
| 90 (res.containsKey(TEXT_STYLE_ID) ? res.getInt(TEXT_STYLE_ID) : R.
style.info_bubble)); |
| 91 |
| 92 setContentView(mTooltipText); |
| 93 setWindowLayoutMode( |
| 94 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP
_CONTENT); |
| 95 } |
| 96 |
| 97 /** |
| 98 * @return The textview for the bubble text. |
| 99 */ |
| 100 public TextView getBubbleTextView() { |
| 101 return mTooltipText; |
| 102 } |
| 103 |
| 104 /** |
| 105 * Shows a text bubble anchored to the given view. |
| 106 * |
| 107 * @param text The text to be shown. |
| 108 * @param anchorView The view that the bubble should be anchored to. |
| 109 * @param maxWidth The maximum width of the text bubble. |
| 110 * @param maxHeight The maximum height of the text bubble. |
| 111 */ |
| 112 public void showTextBubble(String text, View anchorView, int maxWidth, int m
axHeight) { |
| 113 mTooltipText.setText(text); |
| 114 mTooltipText.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.A
T_MOST), |
| 115 MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)); |
| 116 mAnchorView = anchorView; |
| 117 calculateNewPosition(); |
| 118 showAtCalculatedPosition(); |
| 119 } |
| 120 |
| 121 /** |
| 122 * Calculates the new position for the bubble, updating mXPosition, mYPositi
on, mYOffset and |
| 123 * the bubble arrow offset information without updating the UI. To see the c
hanges, |
| 124 * showAtCalculatedPosition should be called explicitly. |
| 125 */ |
| 126 private void calculateNewPosition() { |
| 127 View offsetView = mAnchorView; |
| 128 int xOffset = 0; |
| 129 int yOffset = 0; |
| 130 if (mAnchorBelow) yOffset = mAnchorView.getHeight(); |
| 131 |
| 132 while (offsetView != null) { |
| 133 xOffset += offsetView.getLeft(); |
| 134 yOffset += offsetView.getTop(); |
| 135 if (!(offsetView.getParent() instanceof View)) break; |
| 136 offsetView = (View) offsetView.getParent(); |
| 137 } |
| 138 |
| 139 if (mCenterView) { |
| 140 // Center the tooltip over the view (calculating the width of the to
oltip text). |
| 141 xOffset += mAnchorView.getWidth() / 2; |
| 142 } else if (LocalizationUtils.isLayoutRtl()) { |
| 143 xOffset += mAnchorView.getWidth(); |
| 144 } |
| 145 |
| 146 int tooltipWidth = mTooltipText.getMeasuredWidth(); |
| 147 xOffset -= tooltipWidth / 2; |
| 148 |
| 149 // Account for the padding of the bubble background to ensure it is cent
ered properly. |
| 150 getBackground().getPadding(mCachedPaddingRect); |
| 151 tooltipWidth += mCachedPaddingRect.left + mCachedPaddingRect.right; |
| 152 xOffset -= mCachedPaddingRect.left; |
| 153 |
| 154 int defaultXOffset = xOffset; |
| 155 |
| 156 View rootView = mAnchorView.getRootView(); |
| 157 // Make sure the tooltip does not get rendered off the screen. |
| 158 if (xOffset + tooltipWidth > rootView.getWidth()) { |
| 159 xOffset = rootView.getWidth() - tooltipWidth - mTooltipEdgeMargin; |
| 160 } else if (xOffset < 0) { |
| 161 xOffset = mTooltipEdgeMargin; |
| 162 } |
| 163 |
| 164 // Move the bubble arrow to be centered over the anchor view. |
| 165 int newOffset = -(xOffset - defaultXOffset); |
| 166 if (Math.abs(newOffset) > mTooltipText.getMeasuredWidth() / 2 - mBubbleT
ipXMargin) { |
| 167 newOffset = (mTooltipText.getMeasuredWidth() / 2 - mBubbleTipXMargin
) |
| 168 * (int) Math.signum(newOffset); |
| 169 } |
| 170 ((BubbleBackgroundDrawable) getBackground()).setBubbleArrowXOffset(newOf
fset); |
| 171 |
| 172 if (mAnchorBelow) { |
| 173 mXPosition = xOffset; |
| 174 mYPosition = yOffset - mTooltipTopMargin; |
| 175 } else { |
| 176 mXPosition = xOffset; |
| 177 mYPosition = mAnchorView.getRootView().getHeight() - yOffset + mTool
tipTopMargin; |
| 178 } |
| 179 } |
| 180 |
| 181 /** |
| 182 * Shows the TextBubble in the precalculated position. Should be called afte
r mXPosition |
| 183 * and MYPosition has been set. |
| 184 */ |
| 185 private void showAtCalculatedPosition() { |
| 186 if (mAnchorBelow) { |
| 187 showAtLocation( |
| 188 mAnchorView.getRootView(), Gravity.TOP | Gravity.START, mXPo
sition, mYPosition); |
| 189 } else { |
| 190 showAtLocation(mAnchorView.getRootView(), Gravity.BOTTOM | Gravity.S
TART, mXPosition, |
| 191 mYPosition); |
| 192 } |
| 193 } |
| 194 |
| 195 // The two functions below are used for the floating animation. |
| 196 |
| 197 /** |
| 198 * Updates the y offset of the popup bubble (applied in addition to |
| 199 * the default calculated offset). |
| 200 * @param yoffset The new mYOffset to be used. |
| 201 */ |
| 202 public void setOffsetY(int yoffset) { |
| 203 update(mXPosition, mYPosition + yoffset, -1, -1); |
| 204 } |
| 205 |
| 206 /** |
| 207 * Updates the position information and checks whether any positional change
will occur. This |
| 208 * method doesn't change the {@link TextBubble} if it is showing. |
| 209 * @return Whether the TextBubble needs to be redrawn. |
| 210 */ |
| 211 private boolean updatePosition() { |
| 212 int previousX = mXPosition; |
| 213 int previousY = mYPosition; |
| 214 int previousOffset = ((BubbleBackgroundDrawable) getBackground()).getBub
bleArrowOffset(); |
| 215 calculateNewPosition(); |
| 216 if (previousX != mXPosition || previousY != mYPosition |
| 217 || previousOffset |
| 218 != ((BubbleBackgroundDrawable) getBackground()).getBubbl
eArrowOffset()) { |
| 219 return true; |
| 220 } |
| 221 return false; |
| 222 } |
| 223 |
| 224 @Override |
| 225 public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, |
| 226 int oldTop, int oldRight, int oldBottom) { |
| 227 boolean willDisappear = !mAnchorView.isShown(); |
| 228 boolean changePosition = updatePosition(); |
| 229 if (willDisappear) { |
| 230 dismiss(); |
| 231 } else if (changePosition) { |
| 232 dismiss(); |
| 233 showAtCalculatedPosition(); |
| 234 } |
| 235 } |
| 236 |
| 237 @Override |
| 238 public void onViewAttachedToWindow(View v) {} |
| 239 |
| 240 @Override |
| 241 public void onViewDetachedFromWindow(View v) { |
| 242 dismiss(); |
| 243 } |
| 244 |
| 245 /** |
| 246 * Drawable for rendering the background for a popup bubble. |
| 247 * |
| 248 * <p>Using a custom class as the LayerDrawable handles padding oddly and di
d not allow the |
| 249 * bubble arrow to be rendered below the content portion of the bubble if
you specified |
| 250 * padding, which is required to make it look nice. |
| 251 */ |
| 252 static class BubbleBackgroundDrawable extends Drawable { |
| 253 private final int mTooltipBorderWidth; |
| 254 private final Rect mTooltipContentPadding; |
| 255 |
| 256 private final Drawable mBubbleContentsDrawable; |
| 257 private final BitmapDrawable mBubbleArrowDrawable; |
| 258 private boolean mUp = false; |
| 259 private int mBubbleArrowXOffset; |
| 260 |
| 261 BubbleBackgroundDrawable(Context context, Bundle res) { |
| 262 mUp = (res.containsKey(UP_DOWN) ? res.getBoolean(UP_DOWN) : true); |
| 263 mBubbleContentsDrawable = context.getResources().getDrawable(R.drawa
ble.bubble_white); |
| 264 mBubbleArrowDrawable = (BitmapDrawable) context.getResources().getDr
awable( |
| 265 R.drawable.bubble_point_white); |
| 266 mTooltipBorderWidth = |
| 267 context.getResources().getDimensionPixelSize(R.dimen.tooltip
_border_width); |
| 268 |
| 269 if (res.getBoolean(BACKGROUND_INTRINSIC_PADDING, false)) { |
| 270 mTooltipContentPadding = new Rect(); |
| 271 mBubbleContentsDrawable.getPadding(mTooltipContentPadding); |
| 272 } else { |
| 273 int padding = context.getResources().getDimensionPixelSize( |
| 274 R.dimen.tooltip_content_padding); |
| 275 mTooltipContentPadding = new Rect(padding, padding, padding, pad
ding); |
| 276 } |
| 277 } |
| 278 |
| 279 @Override |
| 280 public void draw(Canvas canvas) { |
| 281 mBubbleContentsDrawable.draw(canvas); |
| 282 mBubbleArrowDrawable.draw(canvas); |
| 283 } |
| 284 |
| 285 @Override |
| 286 protected void onBoundsChange(Rect bounds) { |
| 287 if (bounds == null) return; |
| 288 |
| 289 super.onBoundsChange(bounds); |
| 290 int halfArrowWidth = mBubbleArrowDrawable.getIntrinsicWidth() / 2; |
| 291 int halfBoundsWidth = bounds.width() / 2; |
| 292 if (mUp) { |
| 293 int contentsTop = bounds.top + mBubbleArrowDrawable.getIntrinsic
Height() |
| 294 - mTooltipBorderWidth; |
| 295 mBubbleContentsDrawable.setBounds( |
| 296 bounds.left, contentsTop, bounds.right, bounds.bottom); |
| 297 mBubbleArrowDrawable.setBounds( |
| 298 mBubbleArrowXOffset + halfBoundsWidth - halfArrowWidth,
bounds.top, |
| 299 mBubbleArrowXOffset + halfBoundsWidth + halfArrowWidth, |
| 300 bounds.top + mBubbleArrowDrawable.getIntrinsicHeight()); |
| 301 } else { |
| 302 int contentsBottom = bounds.bottom - mBubbleArrowDrawable.getInt
rinsicHeight(); |
| 303 mBubbleContentsDrawable.setBounds( |
| 304 bounds.left, bounds.left, bounds.right, contentsBottom); |
| 305 mBubbleArrowDrawable.setBounds( |
| 306 mBubbleArrowXOffset + halfBoundsWidth - halfArrowWidth, |
| 307 contentsBottom - mTooltipBorderWidth, |
| 308 mBubbleArrowXOffset + halfBoundsWidth + halfArrowWidth,
contentsBottom |
| 309 + mBubbleArrowDrawable.getIntrinsicHeight() - mT
ooltipBorderWidth); |
| 310 } |
| 311 } |
| 312 |
| 313 @Override |
| 314 public void setAlpha(int alpha) { |
| 315 mBubbleContentsDrawable.setAlpha(alpha); |
| 316 mBubbleArrowDrawable.setAlpha(alpha); |
| 317 } |
| 318 |
| 319 @Override |
| 320 public void setColorFilter(ColorFilter cf) { |
| 321 // Not supported. |
| 322 } |
| 323 |
| 324 @Override |
| 325 public int getOpacity() { |
| 326 return PixelFormat.TRANSLUCENT; |
| 327 } |
| 328 |
| 329 @Override |
| 330 public boolean getPadding(Rect padding) { |
| 331 padding.set(mTooltipContentPadding); |
| 332 if (mUp) { |
| 333 padding.set(padding.left, padding.top + mBubbleArrowDrawable.get
IntrinsicHeight(), |
| 334 padding.right, padding.bottom); |
| 335 } else { |
| 336 padding.set(padding.left, padding.top, padding.right, |
| 337 padding.bottom + mBubbleArrowDrawable.getIntrinsicHeight
()); |
| 338 } |
| 339 |
| 340 return true; |
| 341 } |
| 342 |
| 343 /** |
| 344 * Updates the additional X Offset for the bubble arrow. The arrow defa
ults to being |
| 345 * centered in the bubble, so this is delta from the center. |
| 346 * |
| 347 * @param xOffset The offset of the bubble arrow. |
| 348 */ |
| 349 public void setBubbleArrowXOffset(int xOffset) { |
| 350 mBubbleArrowXOffset = xOffset; |
| 351 onBoundsChange(getBounds()); |
| 352 } |
| 353 |
| 354 /** |
| 355 * @return the current x offset for the bubble arrow. |
| 356 */ |
| 357 public int getBubbleArrowOffset() { |
| 358 return mBubbleArrowXOffset; |
| 359 } |
| 360 } |
| 361 } |
OLD | NEW |