// 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.view.Gravity;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
/**
* UI component that handles showing text bubbles.
*/
public abstract class TextBubble extends PopupWindow implements OnLayoutChangeListener {
/** How much of the anchor should be overlapped. */
private final float mYOverlapPercentage;
private final Rect mCachedPaddingRect = new Rect();
private int mXPosition;
private int mYPosition;
private int mWidth;
private int mHeight;
private View mAnchorView;
private View mContentView;
/**
* Constructs a TextBubble that will point at a particular view.
* @param context Context to draw resources from.
* @param yOverlapPercentage How much the arrow should overlap the view.
*/
public TextBubble(Context context, float yOverlapPercentage) {
super(context);
mYOverlapPercentage = yOverlapPercentage;
setBackgroundDrawable(new BubbleBackgroundDrawable(context));
getBackground().getPadding(mCachedPaddingRect);
mContentView = createContent(context);
setContentView(mContentView);
setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
}
/**
* Creates the View that contains everything that should be displayed inside the bubble.
*/
protected abstract View createContent(Context context);
/**
* Shows a text bubble anchored to the given view.
*
* @param anchorView The view that the bubble should be anchored to.
*/
public void show(View anchorView) {
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() {
measureContentView();
// Center the bubble below of the anchor, arrow pointing upward. The overlap determines how
// much of the bubble's arrow overlaps the anchor view.
int[] anchorCoordinates = {0, 0};
mAnchorView.getLocationOnScreen(anchorCoordinates);
anchorCoordinates[0] += mAnchorView.getWidth() / 2;
anchorCoordinates[1] += (int) (mAnchorView.getHeight() * (1.0 - mYOverlapPercentage));
mWidth = mContentView.getMeasuredWidth()
+ mCachedPaddingRect.left + mCachedPaddingRect.right;
mHeight = mContentView.getMeasuredHeight()
+ mCachedPaddingRect.top + mCachedPaddingRect.bottom;
mXPosition = anchorCoordinates[0] - (mWidth / 2);
mYPosition = anchorCoordinates[1];
// Make sure the bubble stays on screen.
View rootView = mAnchorView.getRootView();
if (mXPosition > rootView.getWidth() - mWidth) {
mXPosition = rootView.getWidth() - mWidth;
} else if (mXPosition < 0) {
mXPosition = 0;
}
// Center the tip of the arrow.
int tipCenterXPosition = anchorCoordinates[0] - mXPosition;
((BubbleBackgroundDrawable) getBackground()).setBubbleArrowXCenter(tipCenterXPosition);
// Update the popup's dimensions.
setWidth(MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY));
setHeight(MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY));
}
private void measureContentView() {
View rootView = mAnchorView.getRootView();
getBackground().getPadding(mCachedPaddingRect);
// The maximum width of the bubble is determined by how wide the root view is.
int maxContentWidth =
rootView.getWidth() - mCachedPaddingRect.left - mCachedPaddingRect.right;
// The maximum height of the bubble is determined by the available space below the anchor.
int anchorYOverlap = (int) -(mYOverlapPercentage * mAnchorView.getHeight());
int maxContentHeight = getMaxAvailableHeight(mAnchorView, anchorYOverlap)
- mCachedPaddingRect.top - mCachedPaddingRect.bottom;
int contentWidthSpec = MeasureSpec.makeMeasureSpec(maxContentWidth, MeasureSpec.AT_MOST);
int contentHeightSpec = MeasureSpec.makeMeasureSpec(maxContentHeight, MeasureSpec.AT_MOST);
mContentView.measure(contentWidthSpec, contentHeightSpec);
}
/**
* Shows the TextBubble in the precalculated position.
*/
private void showAtCalculatedPosition() {
mAnchorView.addOnLayoutChangeListener(this);
showAtLocation(mAnchorView.getRootView(), Gravity.TOP | Gravity.START,
mXPosition, mYPosition);
}
/**
* 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() {
BubbleBackgroundDrawable background = (BubbleBackgroundDrawable) getBackground();
int previousX = mXPosition;
int previousY = mYPosition;
int previousOffset = background.getBubbleArrowXCenter();
calculateNewPosition();
return previousX != mXPosition || previousY != mYPosition
|| previousOffset != background.getBubbleArrowXCenter();
}
@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) {
update(mXPosition, mYPosition, mWidth, mHeight);
}
}
@Override
public void dismiss() {
if (mAnchorView != null) mAnchorView.removeOnLayoutChangeListener(this);
super.dismiss();
}
/**
* Drawable representing a bubble with a arrow pointing upward at something.
*/
private static class BubbleBackgroundDrawable extends Drawable {
private final Drawable mBubbleContentsDrawable;
private final BitmapDrawable mBubbleArrowDrawable;
private int mBubbleArrowXCenter;
BubbleBackgroundDrawable(Context context) {
mBubbleContentsDrawable = ApiCompatibilityUtils.getDrawable(
context.getResources(), R.drawable.menu_bg);
mBubbleArrowDrawable = (BitmapDrawable) ApiCompatibilityUtils.getDrawable(
context.getResources(), R.drawable.bubble_point_white);
}
@Override
public void draw(Canvas canvas) {
mBubbleContentsDrawable.draw(canvas);
mBubbleArrowDrawable.draw(canvas);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
if (bounds == null) return;
// The arrow hugs the top boundary and pushes the rest of the rectangular portion of the
// callout beneath it.
int halfArrowWidth = mBubbleArrowDrawable.getIntrinsicWidth() / 2;
int arrowLeft = mBubbleArrowXCenter + bounds.left - halfArrowWidth;
int arrowRight = arrowLeft + mBubbleArrowDrawable.getIntrinsicWidth();
mBubbleArrowDrawable.setBounds(
arrowLeft,
bounds.top,
arrowRight,
bounds.top + mBubbleArrowDrawable.getIntrinsicHeight());
// Adjust the background of the callout to account for the side margins and the arrow.
Rect bubblePadding = new Rect();
mBubbleContentsDrawable.getPadding(bubblePadding);
mBubbleContentsDrawable.setBounds(
bounds.left,
bounds.top + mBubbleArrowDrawable.getIntrinsicHeight() - bubblePadding.top,
bounds.right,
bounds.bottom);
}
@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) {
mBubbleContentsDrawable.getPadding(padding);
padding.set(padding.left,
Math.max(padding.top, mBubbleArrowDrawable.getIntrinsicHeight()),
padding.right,
padding.bottom);
return true;
}
/**
* Updates where the bubble arrow should be centered along the x-axis.
* @param xOffset The offset of the bubble arrow.
*/
public void setBubbleArrowXCenter(int xOffset) {
mBubbleArrowXCenter = xOffset;
onBoundsChange(getBounds());
}
/**
* @return the current x center for the bubble arrow.
*/
public int getBubbleArrowXCenter() {
return mBubbleArrowXCenter;
}
}
}