// Copyright 2015 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.findinpage;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.findinpage.FindInPageBridge;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.util.MathUtils;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.interpolators.BakedBezierInterpolator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* The view that shows the positions of the find in page matches and allows scrubbing
* between the entries.
*/
class FindResultBar extends View {
private static final int VISIBILITY_ANIMATION_DURATION_MS = 200;
private final int mBackgroundColor;
private final int mBackgroundBorderColor;
private final int mResultColor;
private final int mResultBorderColor;
private final int mActiveColor;
private final int mActiveBorderColor;
private final int mBarTouchWidth;
private final int mBarDrawWidth;
private final int mResultMinHeight;
private final int mActiveMinHeight;
private final int mBarVerticalPadding;
private final int mMinGapBetweenStacks;
private final int mStackedResultHeight;
private final Tab mTab;
private FindInPageBridge mFindInPageBridge;
int mRectsVersion = -1;
private RectF[] mMatches = new RectF[0];
private RectF mActiveMatch;
private ArrayList<Tickmark> mTickmarks = new ArrayList<Tickmark>(0);
private int mBarHeightForWhichTickmarksWereCached = -1;
private Animator mVisibilityAnimation;
private boolean mDismissing;
private final Paint mFillPaint;
private final Paint mStrokePaint;
boolean mWaitingForActivateAck = false;
private static Comparator<RectF> sComparator = new Comparator<RectF>() {
@Override
public int compare(RectF a, RectF b) {
int top = Float.compare(a.top, b.top);
if (top != 0) return top;
return Float.compare(a.left, b.left);
}
};
/**
* Creates an instance of a {@link FindResultBar}.
* @param context The Context to create this {@link FindResultBar} under.
* @param tab The Tab containing the ContentView this {@link FindResultBar} will be drawn in.
*/
public FindResultBar(Context context, Tab tab, FindInPageBridge findInPageBridge) {
super(context);
Resources res = context.getResources();
mBackgroundColor = ApiCompatibilityUtils.getColor(res,
R.color.find_result_bar_background_color);
mBackgroundBorderColor = ApiCompatibilityUtils.getColor(res,
R.color.find_result_bar_background_border_color);
mResultColor = ApiCompatibilityUtils.getColor(res,
R.color.find_result_bar_result_color);
mResultBorderColor = ApiCompatibilityUtils.getColor(res,
R.color.find_result_bar_result_border_color);
mActiveColor = ApiCompatibilityUtils.getColor(res,
R.color.find_result_bar_active_color);
mActiveBorderColor = ApiCompatibilityUtils.getColor(res,
R.color.find_result_bar_active_border_color);
mBarTouchWidth = res.getDimensionPixelSize(
R.dimen.find_result_bar_touch_width);
mBarDrawWidth = res.getDimensionPixelSize(R.dimen.find_result_bar_draw_width)
+ res.getDimensionPixelSize(R.dimen.find_in_page_separator_width);
mResultMinHeight = res.getDimensionPixelSize(R.dimen.find_result_bar_result_min_height);
mActiveMinHeight = res.getDimensionPixelSize(
R.dimen.find_result_bar_active_min_height);
mBarVerticalPadding = res.getDimensionPixelSize(
R.dimen.find_result_bar_vertical_padding);
mMinGapBetweenStacks = res.getDimensionPixelSize(
R.dimen.find_result_bar_min_gap_between_stacks);
mStackedResultHeight = res.getDimensionPixelSize(
R.dimen.find_result_bar_stacked_result_height);
mFillPaint = new Paint();
mStrokePaint = new Paint();
mFillPaint.setAntiAlias(true);
mStrokePaint.setAntiAlias(true);
mFillPaint.setStyle(Paint.Style.FILL);
mStrokePaint.setStyle(Paint.Style.STROKE);
mStrokePaint.setStrokeWidth(1.0f);
mFindInPageBridge = findInPageBridge;
mTab = tab;
mTab.getContentViewCore().getContainerView().addView(
this, new FrameLayout.LayoutParams(mBarTouchWidth,
ViewGroup.LayoutParams.MATCH_PARENT, Gravity.END));
setTranslationX(
MathUtils.flipSignIf(mBarTouchWidth, LocalizationUtils.isLayoutRtl()));
mVisibilityAnimation = ObjectAnimator.ofFloat(this, TRANSLATION_X, 0);
mVisibilityAnimation.setDuration(VISIBILITY_ANIMATION_DURATION_MS);
mVisibilityAnimation.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
mTab.getWindowAndroid().startAnimationOverContent(mVisibilityAnimation);
}
/** Dismisses this results bar by removing it from the view hierarchy. */
public void dismiss() {
mDismissing = true;
mFindInPageBridge = null;
if (mVisibilityAnimation != null && mVisibilityAnimation.isRunning()) {
mVisibilityAnimation.cancel();
}
mVisibilityAnimation = ObjectAnimator.ofFloat(this, TRANSLATION_X,
MathUtils.flipSignIf(mBarTouchWidth, LocalizationUtils.isLayoutRtl()));
mVisibilityAnimation.setDuration(VISIBILITY_ANIMATION_DURATION_MS);
mVisibilityAnimation.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);
mTab.getWindowAndroid().startAnimationOverContent(mVisibilityAnimation);
mVisibilityAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (getParent() != null) ((ViewGroup) getParent()).removeView(FindResultBar.this);
}
});
}
/** Setup the tickmarks to draw using the rects of the find results. */
public void setMatchRects(int version, RectF[] rects, RectF activeRect) {
if (mRectsVersion != version) {
mRectsVersion = version;
assert rects != null;
mMatches = rects;
mTickmarks.clear();
Arrays.sort(mMatches, sComparator);
mBarHeightForWhichTickmarksWereCached = -1;
}
mActiveMatch = activeRect; // Can be null.
invalidate();
}
/** Clears the tickmarks. */
public void clearMatchRects() {
setMatchRects(-1, new RectF[0], null);
}
@Override
@SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(MotionEvent event) {
if (!mDismissing && mTickmarks.size() > 0 && mTickmarks.size() == mMatches.length
&& !mWaitingForActivateAck && event.getAction() != MotionEvent.ACTION_CANCEL) {
// We decided it's more important to get the keyboard out of the
// way asap; the user can compensate if their next MotionEvent
// scrolls somewhere unintended.
UiUtils.hideKeyboard(this);
// Identify which drawn tickmark is closest to the user's finger.
int closest = Collections.binarySearch(mTickmarks,
new Tickmark(event.getY(), event.getY()));
if (closest < 0) {
// No exact match, so must determine nearest.
int insertionPoint = -1 - closest;
if (insertionPoint == 0) {
closest = 0;
} else if (insertionPoint == mTickmarks.size()) {
closest = mTickmarks.size() - 1;
} else {
float distanceA = Math.abs(event.getY()
- mTickmarks.get(insertionPoint - 1).centerY());
float distanceB = Math.abs(event.getY()
- mTickmarks.get(insertionPoint).centerY());
closest = insertionPoint - (distanceA <= distanceB ? 1 : 0);
}
}
// Now activate the find match corresponding to that tickmark.
// Since mTickmarks may be outdated, we can't just pass the index.
// Instead we send the renderer the coordinates of the center of the
// find match's rect (as originally received in setMatchRects), and
// it will activate whatever find result is currently closest to
// that point (which will usually be the same one).
mWaitingForActivateAck = true;
mFindInPageBridge.activateNearestFindResult(
mMatches[closest].centerX(),
mMatches[closest].centerY());
}
return true; // Consume the event, whether or not we acted upon it.
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Check for new rects, as they may move if the document size changes.
if (!mDismissing && mMatches.length > 0) {
mFindInPageBridge.requestFindMatchRects(mRectsVersion);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int leftMargin = getLeftMargin();
mFillPaint.setColor(mBackgroundColor);
mStrokePaint.setColor(mBackgroundBorderColor);
canvas.drawRect(leftMargin, 0,
leftMargin + mBarDrawWidth, getHeight(), mFillPaint);
float lineX = LocalizationUtils.isLayoutRtl()
? leftMargin + mBarDrawWidth - 0.5f
: leftMargin + 0.5f;
canvas.drawLine(lineX, 0, lineX, getHeight(), mStrokePaint);
if (mMatches.length == 0) {
return;
}
if (mBarHeightForWhichTickmarksWereCached != getHeight()) {
calculateTickmarks();
}
// Draw all matches (since they're sorted by increasing y-position
// overlapping tickmarks will form nice stacks).
mFillPaint.setColor(mResultColor);
mStrokePaint.setColor(mResultBorderColor);
for (Tickmark tickmark : mTickmarks) {
RectF rect = tickmark.toRectF();
canvas.drawRoundRect(rect, 2, 2, mFillPaint);
canvas.drawRoundRect(rect, 2, 2, mStrokePaint);
}
// Draw the active tickmark on top (covering up the inactive tickmark
// we probably already drew for it).
if (mActiveMatch != null) {
Tickmark tickmark;
int i = Arrays.binarySearch(mMatches, mActiveMatch, sComparator);
if (i >= 0) {
// We've already generated a tickmark for all rects in mMatches,
// so use the corresponding one. However it was generated
// assuming the match would be inactive. Keep the position, but
// re-expand it using mActiveMinHeight.
tickmark = expandTickmarkToMinHeight(mTickmarks.get(i), true);
} else {
// How strange - mActiveMatch isn't in mMatches. Do our best to
// draw it anyway (though it might not line up exactly).
tickmark = tickmarkForRect(mActiveMatch, true);
}
RectF rect = tickmark.toRectF();
mFillPaint.setColor(mActiveColor);
mStrokePaint.setColor(mActiveBorderColor);
canvas.drawRoundRect(rect, 2, 2, mFillPaint);
canvas.drawRoundRect(rect, 2, 2, mStrokePaint);
}
}
private int getLeftMargin() {
return LocalizationUtils.isLayoutRtl() ? 0 : getWidth() - mBarDrawWidth;
}
private void calculateTickmarks() {
// TODO(johnme): Simplify calculation, and switch to integer arithmetic
// where possible (tickmarks within groups will still need fractional
// y-positions for anti-aliasing, but the start and end positions of
// groups can and should be integer-aligned [will give crisp borders],
// and the intermediary logic uses more floats than necessary).
// TODO(johnme): Consider adding unit tests for this.
mBarHeightForWhichTickmarksWereCached = getHeight();
// Generate tickmarks, neatly clustering any overlapping matches.
mTickmarks = new ArrayList<Tickmark>(mMatches.length);
int i = 0;
Tickmark nextTickmark = tickmarkForRect(mMatches[i], false);
float lastGroupEnd = -mMinGapBetweenStacks;
while (i < mMatches.length) {
// Find next cluster of overlapping tickmarks.
List<Tickmark> cluster = new ArrayList<Tickmark>();
cluster.add(nextTickmark);
i++;
while (i < mMatches.length) {
nextTickmark = tickmarkForRect(mMatches[i], false);
if (nextTickmark.mTop <= cluster.get(cluster.size() - 1).mBottom
+ mMinGapBetweenStacks) {
cluster.add(nextTickmark);
i++;
} else {
break;
}
}
// Draw cluster.
int cn = cluster.size();
float minStart = lastGroupEnd + mMinGapBetweenStacks;
lastGroupEnd = cluster.get(cn - 1).mBottom;
float preferredStart = lastGroupEnd
- (cn - 1) * mStackedResultHeight
- mResultMinHeight;
float maxStart = cluster.get(0).mTop;
float start = Math.round(MathUtils.clamp(preferredStart, minStart, maxStart));
float scale = start >= preferredStart ? 1.0f :
(lastGroupEnd - start) / (lastGroupEnd - preferredStart);
float spacing = cn == 1 ? 0 : (lastGroupEnd - start
- scale * mResultMinHeight) / (cn - 1);
for (int j = 0; j < cn; j++) {
Tickmark tickmark = cluster.get(j);
tickmark.mTop = start + j * spacing;
if (j != cn - 1) {
tickmark.mBottom = tickmark.mTop + scale * mResultMinHeight;
}
mTickmarks.add(tickmark);
}
}
}
private Tickmark tickmarkForRect(RectF r, boolean active) {
// Ratio of results bar height to page height
float vScale = mBarHeightForWhichTickmarksWereCached - 2 * mBarVerticalPadding;
Tickmark tickmark = new Tickmark(
r.top * vScale + mBarVerticalPadding,
r.bottom * vScale + mBarVerticalPadding);
return expandTickmarkToMinHeight(tickmark, active);
}
private Tickmark expandTickmarkToMinHeight(Tickmark tickmark,
boolean active) {
int minHeight = active ? mActiveMinHeight : mResultMinHeight;
float missingHeight = minHeight - tickmark.height();
if (missingHeight > 0) {
return new Tickmark(tickmark.mTop - missingHeight / 2.0f,
tickmark.mBottom + missingHeight / 2.0f);
}
return tickmark;
}
/** Like android.graphics.RectF, but without a left or right. */
private class Tickmark implements Comparable<Tickmark> {
float mTop;
float mBottom;
Tickmark(float top, float bottom) {
this.mTop = top;
this.mBottom = bottom;
}
float height() {
return mBottom - mTop;
}
float centerY() {
return (mTop + mBottom) * 0.5f;
}
RectF toRectF() {
int leftMargin = getLeftMargin();
RectF rect = new RectF(leftMargin, mTop, leftMargin + mBarDrawWidth, mBottom);
rect.inset(2.0f, 0.5f);
rect.offset(LocalizationUtils.isLayoutRtl() ? -0.5f : 0.5f, 0);
return rect;
}
@SuppressFBWarnings("EQ_COMPARETO_USE_OBJECT_EQUAL")
@Override
public int compareTo(Tickmark other) {
return Float.compare(centerY(), other.centerY());
}
}
}