| Index: chrome/android/java_staging/src/org/chromium/chrome/browser/widget/findinpage/FindResultBar.java
|
| diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/widget/findinpage/FindResultBar.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/widget/findinpage/FindResultBar.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..1645cf45233bf18c359e62429d99167ef8ebe8b9
|
| --- /dev/null
|
| +++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/widget/findinpage/FindResultBar.java
|
| @@ -0,0 +1,402 @@
|
| +// 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 com.google.android.apps.chrome.R;
|
| +
|
| +import org.chromium.base.annotations.SuppressFBWarnings;
|
| +import org.chromium.chrome.browser.Tab;
|
| +import org.chromium.chrome.browser.findinpage.FindInPageBridge;
|
| +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 VISIBILTY_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) {
|
| + if (a.top != b.top) return a.top > b.top ? 1 : -1;
|
| + if (a.top != b.top) return a.left > b.left ? 1 : -1;
|
| + return 0;
|
| + }
|
| + };
|
| +
|
| + /**
|
| + * 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 = res.getColor(
|
| + R.color.find_result_bar_background_color);
|
| + mBackgroundBorderColor = res.getColor(
|
| + R.color.find_result_bar_background_border_color);
|
| + mResultColor = res.getColor(
|
| + R.color.find_result_bar_result_color);
|
| + mResultBorderColor = res.getColor(
|
| + R.color.find_result_bar_result_border_color);
|
| + mActiveColor = res.getColor(
|
| + R.color.find_result_bar_active_color);
|
| + mActiveBorderColor = res.getColor(
|
| + 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(VISIBILTY_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;
|
| + if (mVisibilityAnimation != null && mVisibilityAnimation.isRunning()) {
|
| + mVisibilityAnimation.cancel();
|
| + }
|
| +
|
| + mVisibilityAnimation = ObjectAnimator.ofFloat(this, TRANSLATION_X,
|
| + MathUtils.flipSignIf(mBarTouchWidth, LocalizationUtils.isLayoutRtl()));
|
| + mVisibilityAnimation.setDuration(VISIBILTY_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) {
|
| + float center = centerY();
|
| + float otherCenter = other.centerY();
|
| + if (center == otherCenter) return 0;
|
| + return center > otherCenter ? 1 : -1;
|
| + }
|
| + }
|
| +}
|
|
|