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; |
+ } |
+ } |
+} |