Index: chrome/android/java/src/org/chromium/chrome/browser/widget/TextBubble.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/widget/TextBubble.java b/chrome/android/java/src/org/chromium/chrome/browser/widget/TextBubble.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8e38cbaa0e19d27acf1b1a06d0ef4d66ec780491 |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/widget/TextBubble.java |
@@ -0,0 +1,361 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.widget; |
+ |
+import android.content.Context; |
+import android.graphics.Canvas; |
+import android.graphics.ColorFilter; |
+import android.graphics.PixelFormat; |
+import android.graphics.Rect; |
+import android.graphics.drawable.BitmapDrawable; |
+import android.graphics.drawable.Drawable; |
+import android.os.Bundle; |
+import android.view.Gravity; |
+import android.view.View; |
+import android.view.View.MeasureSpec; |
+import android.view.View.OnAttachStateChangeListener; |
+import android.view.View.OnLayoutChangeListener; |
+import android.view.ViewGroup; |
+import android.widget.PopupWindow; |
+import android.widget.TextView; |
+ |
+import org.chromium.chrome.R; |
+import org.chromium.ui.base.LocalizationUtils; |
+ |
+/** |
+ * UI component that handles showing text bubbles. |
+ */ |
+public class TextBubble |
+ extends PopupWindow implements OnLayoutChangeListener, OnAttachStateChangeListener { |
+ /** Whether to use the intrinsic padding of the bubble background as padding (boolean). */ |
+ public static final String BACKGROUND_INTRINSIC_PADDING = "Background_Intrinsic_Padding"; |
+ |
+ /** |
+ * Boolean to be used for deciding whether the bubble should be anchored above or below |
+ * the view |
+ */ |
+ public static final String UP_DOWN = "Up_Down"; |
+ |
+ /** Style resource Id to be used for text inside the bubble. Should be of type int. */ |
+ public static final String TEXT_STYLE_ID = "Text_Style_Id"; |
+ |
+ /** Boolean to be used for deciding whether the bubble should be centered to the view */ |
+ public static final String CENTER = "Center"; |
+ |
+ public static final String ANIM_STYLE_ID = "Animation_Style"; |
+ |
+ private final int mTooltipEdgeMargin; |
+ private final int mTooltipTopMargin; |
+ private final int mBubbleTipXMargin; |
+ private boolean mAnchorBelow = false; |
+ private boolean mCenterView = true; |
+ private int mXPosition; |
+ private int mYPosition; |
+ private View mAnchorView; |
+ private final Rect mCachedPaddingRect = new Rect(); |
+ |
+ // The text view inside the popup containing the tooltip text. |
+ private final TextView mTooltipText; |
+ |
+ /** |
+ * Constructor that uses a bundle object to fetch resources and optional boolean |
+ * values for the {@link TextBubble}. |
+ * |
+ * Use CENTER for centering the tip to the anchor view. |
+ * UP_DOWN for drawing the bubble with tip pointing up or down. |
+ * Up is true and Down is false. |
+ * LAYOUT_WIDTH_ID Dimension resource Id for the width of the {@link TextView} inside the |
+ * bubble. The height is set to half of this value. |
+ * |
+ * @param context |
+ * @param res Bundle object that contains resource ids and optional flags. |
+ */ |
+ public TextBubble(Context context, Bundle res) { |
+ mAnchorBelow = (res.containsKey(UP_DOWN) ? res.getBoolean(UP_DOWN) : true); |
+ mCenterView = (res.containsKey(CENTER) ? res.getBoolean(CENTER) : true); |
+ mTooltipEdgeMargin = |
+ context.getResources().getDimensionPixelSize(R.dimen.tooltip_min_edge_margin); |
+ mTooltipTopMargin = |
+ context.getResources().getDimensionPixelSize(R.dimen.tooltip_top_margin); |
+ mBubbleTipXMargin = context.getResources().getDimensionPixelSize(R.dimen.bubble_tip_margin); |
+ |
+ setBackgroundDrawable(new BubbleBackgroundDrawable(context, res)); |
+ setAnimationStyle(res.containsKey(ANIM_STYLE_ID) ? res.getInt(ANIM_STYLE_ID) |
+ : android.R.style.Animation); |
+ |
+ mTooltipText = new TextView(context); |
+ mTooltipText.setTextAppearance(context, |
+ (res.containsKey(TEXT_STYLE_ID) ? res.getInt(TEXT_STYLE_ID) : R.style.info_bubble)); |
+ |
+ setContentView(mTooltipText); |
+ setWindowLayoutMode( |
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
+ } |
+ |
+ /** |
+ * @return The textview for the bubble text. |
+ */ |
+ public TextView getBubbleTextView() { |
+ return mTooltipText; |
+ } |
+ |
+ /** |
+ * Shows a text bubble anchored to the given view. |
+ * |
+ * @param text The text to be shown. |
+ * @param anchorView The view that the bubble should be anchored to. |
+ * @param maxWidth The maximum width of the text bubble. |
+ * @param maxHeight The maximum height of the text bubble. |
+ */ |
+ public void showTextBubble(String text, View anchorView, int maxWidth, int maxHeight) { |
+ mTooltipText.setText(text); |
+ mTooltipText.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST), |
+ MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)); |
+ mAnchorView = anchorView; |
+ calculateNewPosition(); |
+ showAtCalculatedPosition(); |
+ } |
+ |
+ /** |
+ * Calculates the new position for the bubble, updating mXPosition, mYPosition, mYOffset and |
+ * the bubble arrow offset information without updating the UI. To see the changes, |
+ * showAtCalculatedPosition should be called explicitly. |
+ */ |
+ private void calculateNewPosition() { |
+ View offsetView = mAnchorView; |
+ int xOffset = 0; |
+ int yOffset = 0; |
+ if (mAnchorBelow) yOffset = mAnchorView.getHeight(); |
+ |
+ while (offsetView != null) { |
+ xOffset += offsetView.getLeft(); |
+ yOffset += offsetView.getTop(); |
+ if (!(offsetView.getParent() instanceof View)) break; |
+ offsetView = (View) offsetView.getParent(); |
+ } |
+ |
+ if (mCenterView) { |
+ // Center the tooltip over the view (calculating the width of the tooltip text). |
+ xOffset += mAnchorView.getWidth() / 2; |
+ } else if (LocalizationUtils.isLayoutRtl()) { |
+ xOffset += mAnchorView.getWidth(); |
+ } |
+ |
+ int tooltipWidth = mTooltipText.getMeasuredWidth(); |
+ xOffset -= tooltipWidth / 2; |
+ |
+ // Account for the padding of the bubble background to ensure it is centered properly. |
+ getBackground().getPadding(mCachedPaddingRect); |
+ tooltipWidth += mCachedPaddingRect.left + mCachedPaddingRect.right; |
+ xOffset -= mCachedPaddingRect.left; |
+ |
+ int defaultXOffset = xOffset; |
+ |
+ View rootView = mAnchorView.getRootView(); |
+ // Make sure the tooltip does not get rendered off the screen. |
+ if (xOffset + tooltipWidth > rootView.getWidth()) { |
+ xOffset = rootView.getWidth() - tooltipWidth - mTooltipEdgeMargin; |
+ } else if (xOffset < 0) { |
+ xOffset = mTooltipEdgeMargin; |
+ } |
+ |
+ // Move the bubble arrow to be centered over the anchor view. |
+ int newOffset = -(xOffset - defaultXOffset); |
+ if (Math.abs(newOffset) > mTooltipText.getMeasuredWidth() / 2 - mBubbleTipXMargin) { |
+ newOffset = (mTooltipText.getMeasuredWidth() / 2 - mBubbleTipXMargin) |
+ * (int) Math.signum(newOffset); |
+ } |
+ ((BubbleBackgroundDrawable) getBackground()).setBubbleArrowXOffset(newOffset); |
+ |
+ if (mAnchorBelow) { |
+ mXPosition = xOffset; |
+ mYPosition = yOffset - mTooltipTopMargin; |
+ } else { |
+ mXPosition = xOffset; |
+ mYPosition = mAnchorView.getRootView().getHeight() - yOffset + mTooltipTopMargin; |
+ } |
+ } |
+ |
+ /** |
+ * Shows the TextBubble in the precalculated position. Should be called after mXPosition |
+ * and MYPosition has been set. |
+ */ |
+ private void showAtCalculatedPosition() { |
+ if (mAnchorBelow) { |
+ showAtLocation( |
+ mAnchorView.getRootView(), Gravity.TOP | Gravity.START, mXPosition, mYPosition); |
+ } else { |
+ showAtLocation(mAnchorView.getRootView(), Gravity.BOTTOM | Gravity.START, mXPosition, |
+ mYPosition); |
+ } |
+ } |
+ |
+ // The two functions below are used for the floating animation. |
+ |
+ /** |
+ * Updates the y offset of the popup bubble (applied in addition to |
+ * the default calculated offset). |
+ * @param yoffset The new mYOffset to be used. |
+ */ |
+ public void setOffsetY(int yoffset) { |
+ update(mXPosition, mYPosition + yoffset, -1, -1); |
+ } |
+ |
+ /** |
+ * Updates the position information and checks whether any positional change will occur. This |
+ * method doesn't change the {@link TextBubble} if it is showing. |
+ * @return Whether the TextBubble needs to be redrawn. |
+ */ |
+ private boolean updatePosition() { |
+ int previousX = mXPosition; |
+ int previousY = mYPosition; |
+ int previousOffset = ((BubbleBackgroundDrawable) getBackground()).getBubbleArrowOffset(); |
+ calculateNewPosition(); |
+ if (previousX != mXPosition || previousY != mYPosition |
+ || previousOffset |
+ != ((BubbleBackgroundDrawable) getBackground()).getBubbleArrowOffset()) { |
+ return true; |
+ } |
+ return false; |
+ } |
+ |
+ @Override |
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, |
+ int oldTop, int oldRight, int oldBottom) { |
+ boolean willDisappear = !mAnchorView.isShown(); |
+ boolean changePosition = updatePosition(); |
+ if (willDisappear) { |
+ dismiss(); |
+ } else if (changePosition) { |
+ dismiss(); |
+ showAtCalculatedPosition(); |
+ } |
+ } |
+ |
+ @Override |
+ public void onViewAttachedToWindow(View v) {} |
+ |
+ @Override |
+ public void onViewDetachedFromWindow(View v) { |
+ dismiss(); |
+ } |
+ |
+ /** |
+ * Drawable for rendering the background for a popup bubble. |
+ * |
+ * <p>Using a custom class as the LayerDrawable handles padding oddly and did not allow the |
+ * bubble arrow to be rendered below the content portion of the bubble if you specified |
+ * padding, which is required to make it look nice. |
+ */ |
+ static class BubbleBackgroundDrawable extends Drawable { |
+ private final int mTooltipBorderWidth; |
+ private final Rect mTooltipContentPadding; |
+ |
+ private final Drawable mBubbleContentsDrawable; |
+ private final BitmapDrawable mBubbleArrowDrawable; |
+ private boolean mUp = false; |
+ private int mBubbleArrowXOffset; |
+ |
+ BubbleBackgroundDrawable(Context context, Bundle res) { |
+ mUp = (res.containsKey(UP_DOWN) ? res.getBoolean(UP_DOWN) : true); |
+ mBubbleContentsDrawable = context.getResources().getDrawable(R.drawable.bubble_white); |
+ mBubbleArrowDrawable = (BitmapDrawable) context.getResources().getDrawable( |
+ R.drawable.bubble_point_white); |
+ mTooltipBorderWidth = |
+ context.getResources().getDimensionPixelSize(R.dimen.tooltip_border_width); |
+ |
+ if (res.getBoolean(BACKGROUND_INTRINSIC_PADDING, false)) { |
+ mTooltipContentPadding = new Rect(); |
+ mBubbleContentsDrawable.getPadding(mTooltipContentPadding); |
+ } else { |
+ int padding = context.getResources().getDimensionPixelSize( |
+ R.dimen.tooltip_content_padding); |
+ mTooltipContentPadding = new Rect(padding, padding, padding, padding); |
+ } |
+ } |
+ |
+ @Override |
+ public void draw(Canvas canvas) { |
+ mBubbleContentsDrawable.draw(canvas); |
+ mBubbleArrowDrawable.draw(canvas); |
+ } |
+ |
+ @Override |
+ protected void onBoundsChange(Rect bounds) { |
+ if (bounds == null) return; |
+ |
+ super.onBoundsChange(bounds); |
+ int halfArrowWidth = mBubbleArrowDrawable.getIntrinsicWidth() / 2; |
+ int halfBoundsWidth = bounds.width() / 2; |
+ if (mUp) { |
+ int contentsTop = bounds.top + mBubbleArrowDrawable.getIntrinsicHeight() |
+ - mTooltipBorderWidth; |
+ mBubbleContentsDrawable.setBounds( |
+ bounds.left, contentsTop, bounds.right, bounds.bottom); |
+ mBubbleArrowDrawable.setBounds( |
+ mBubbleArrowXOffset + halfBoundsWidth - halfArrowWidth, bounds.top, |
+ mBubbleArrowXOffset + halfBoundsWidth + halfArrowWidth, |
+ bounds.top + mBubbleArrowDrawable.getIntrinsicHeight()); |
+ } else { |
+ int contentsBottom = bounds.bottom - mBubbleArrowDrawable.getIntrinsicHeight(); |
+ mBubbleContentsDrawable.setBounds( |
+ bounds.left, bounds.left, bounds.right, contentsBottom); |
+ mBubbleArrowDrawable.setBounds( |
+ mBubbleArrowXOffset + halfBoundsWidth - halfArrowWidth, |
+ contentsBottom - mTooltipBorderWidth, |
+ mBubbleArrowXOffset + halfBoundsWidth + halfArrowWidth, contentsBottom |
+ + mBubbleArrowDrawable.getIntrinsicHeight() - mTooltipBorderWidth); |
+ } |
+ } |
+ |
+ @Override |
+ public void setAlpha(int alpha) { |
+ mBubbleContentsDrawable.setAlpha(alpha); |
+ mBubbleArrowDrawable.setAlpha(alpha); |
+ } |
+ |
+ @Override |
+ public void setColorFilter(ColorFilter cf) { |
+ // Not supported. |
+ } |
+ |
+ @Override |
+ public int getOpacity() { |
+ return PixelFormat.TRANSLUCENT; |
+ } |
+ |
+ @Override |
+ public boolean getPadding(Rect padding) { |
+ padding.set(mTooltipContentPadding); |
+ if (mUp) { |
+ padding.set(padding.left, padding.top + mBubbleArrowDrawable.getIntrinsicHeight(), |
+ padding.right, padding.bottom); |
+ } else { |
+ padding.set(padding.left, padding.top, padding.right, |
+ padding.bottom + mBubbleArrowDrawable.getIntrinsicHeight()); |
+ } |
+ |
+ return true; |
+ } |
+ |
+ /** |
+ * Updates the additional X Offset for the bubble arrow. The arrow defaults to being |
+ * centered in the bubble, so this is delta from the center. |
+ * |
+ * @param xOffset The offset of the bubble arrow. |
+ */ |
+ public void setBubbleArrowXOffset(int xOffset) { |
+ mBubbleArrowXOffset = xOffset; |
+ onBoundsChange(getBounds()); |
+ } |
+ |
+ /** |
+ * @return the current x offset for the bubble arrow. |
+ */ |
+ public int getBubbleArrowOffset() { |
+ return mBubbleArrowXOffset; |
+ } |
+ } |
+} |