OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.widget.findinpage; |
| 6 |
| 7 import android.animation.Animator; |
| 8 import android.animation.AnimatorListenerAdapter; |
| 9 import android.animation.ObjectAnimator; |
| 10 import android.annotation.SuppressLint; |
| 11 import android.content.Context; |
| 12 import android.content.res.Resources; |
| 13 import android.graphics.Canvas; |
| 14 import android.graphics.Paint; |
| 15 import android.graphics.RectF; |
| 16 import android.view.Gravity; |
| 17 import android.view.MotionEvent; |
| 18 import android.view.View; |
| 19 import android.view.ViewGroup; |
| 20 import android.widget.FrameLayout; |
| 21 |
| 22 import com.google.android.apps.chrome.R; |
| 23 |
| 24 import org.chromium.base.annotations.SuppressFBWarnings; |
| 25 import org.chromium.chrome.browser.Tab; |
| 26 import org.chromium.chrome.browser.findinpage.FindInPageBridge; |
| 27 import org.chromium.chrome.browser.util.MathUtils; |
| 28 import org.chromium.ui.UiUtils; |
| 29 import org.chromium.ui.base.LocalizationUtils; |
| 30 import org.chromium.ui.interpolators.BakedBezierInterpolator; |
| 31 |
| 32 import java.util.ArrayList; |
| 33 import java.util.Arrays; |
| 34 import java.util.Collections; |
| 35 import java.util.Comparator; |
| 36 import java.util.List; |
| 37 |
| 38 /** |
| 39 * The view that shows the positions of the find in page matches and allows scru
bbing |
| 40 * between the entries. |
| 41 */ |
| 42 class FindResultBar extends View { |
| 43 private static final int VISIBILTY_ANIMATION_DURATION_MS = 200; |
| 44 |
| 45 private final int mBackgroundColor; |
| 46 private final int mBackgroundBorderColor; |
| 47 private final int mResultColor; |
| 48 private final int mResultBorderColor; |
| 49 private final int mActiveColor; |
| 50 private final int mActiveBorderColor; |
| 51 |
| 52 private final int mBarTouchWidth; |
| 53 private final int mBarDrawWidth; |
| 54 private final int mResultMinHeight; |
| 55 private final int mActiveMinHeight; |
| 56 private final int mBarVerticalPadding; |
| 57 private final int mMinGapBetweenStacks; |
| 58 private final int mStackedResultHeight; |
| 59 |
| 60 private final Tab mTab; |
| 61 private FindInPageBridge mFindInPageBridge; |
| 62 |
| 63 int mRectsVersion = -1; |
| 64 private RectF[] mMatches = new RectF[0]; |
| 65 private RectF mActiveMatch; |
| 66 |
| 67 private ArrayList<Tickmark> mTickmarks = new ArrayList<Tickmark>(0); |
| 68 private int mBarHeightForWhichTickmarksWereCached = -1; |
| 69 |
| 70 private Animator mVisibilityAnimation; |
| 71 private boolean mDismissing; |
| 72 |
| 73 private final Paint mFillPaint; |
| 74 private final Paint mStrokePaint; |
| 75 |
| 76 boolean mWaitingForActivateAck = false; |
| 77 |
| 78 private static Comparator<RectF> sComparator = new Comparator<RectF>() { |
| 79 @Override |
| 80 public int compare(RectF a, RectF b) { |
| 81 if (a.top != b.top) return a.top > b.top ? 1 : -1; |
| 82 if (a.top != b.top) return a.left > b.left ? 1 : -1; |
| 83 return 0; |
| 84 } |
| 85 }; |
| 86 |
| 87 /** |
| 88 * Creates an instance of a {@link FindResultBar}. |
| 89 * @param context The Context to create this {@link FindResultBar} under. |
| 90 * @param tab The Tab containing the ContentView this {@link FindResultBar}
will be drawn in. |
| 91 */ |
| 92 public FindResultBar(Context context, Tab tab, FindInPageBridge findInPageBr
idge) { |
| 93 super(context); |
| 94 |
| 95 Resources res = context.getResources(); |
| 96 mBackgroundColor = res.getColor( |
| 97 R.color.find_result_bar_background_color); |
| 98 mBackgroundBorderColor = res.getColor( |
| 99 R.color.find_result_bar_background_border_color); |
| 100 mResultColor = res.getColor( |
| 101 R.color.find_result_bar_result_color); |
| 102 mResultBorderColor = res.getColor( |
| 103 R.color.find_result_bar_result_border_color); |
| 104 mActiveColor = res.getColor( |
| 105 R.color.find_result_bar_active_color); |
| 106 mActiveBorderColor = res.getColor( |
| 107 R.color.find_result_bar_active_border_color); |
| 108 mBarTouchWidth = res.getDimensionPixelSize( |
| 109 R.dimen.find_result_bar_touch_width); |
| 110 mBarDrawWidth = res.getDimensionPixelSize(R.dimen.find_result_bar_draw_w
idth) |
| 111 + res.getDimensionPixelSize(R.dimen.find_in_page_separator_width
); |
| 112 mResultMinHeight = res.getDimensionPixelSize(R.dimen.find_result_bar_res
ult_min_height); |
| 113 mActiveMinHeight = res.getDimensionPixelSize( |
| 114 R.dimen.find_result_bar_active_min_height); |
| 115 mBarVerticalPadding = res.getDimensionPixelSize( |
| 116 R.dimen.find_result_bar_vertical_padding); |
| 117 mMinGapBetweenStacks = res.getDimensionPixelSize( |
| 118 R.dimen.find_result_bar_min_gap_between_stacks); |
| 119 mStackedResultHeight = res.getDimensionPixelSize( |
| 120 R.dimen.find_result_bar_stacked_result_height); |
| 121 |
| 122 mFillPaint = new Paint(); |
| 123 mStrokePaint = new Paint(); |
| 124 mFillPaint.setAntiAlias(true); |
| 125 mStrokePaint.setAntiAlias(true); |
| 126 mFillPaint.setStyle(Paint.Style.FILL); |
| 127 mStrokePaint.setStyle(Paint.Style.STROKE); |
| 128 mStrokePaint.setStrokeWidth(1.0f); |
| 129 |
| 130 mFindInPageBridge = findInPageBridge; |
| 131 mTab = tab; |
| 132 mTab.getContentViewCore().getContainerView().addView( |
| 133 this, new FrameLayout.LayoutParams(mBarTouchWidth, |
| 134 ViewGroup.LayoutParams.MATCH_PARENT, Gravity.END)); |
| 135 setTranslationX( |
| 136 MathUtils.flipSignIf(mBarTouchWidth, LocalizationUtils.isLayoutR
tl())); |
| 137 |
| 138 mVisibilityAnimation = ObjectAnimator.ofFloat(this, TRANSLATION_X, 0); |
| 139 mVisibilityAnimation.setDuration(VISIBILTY_ANIMATION_DURATION_MS); |
| 140 mVisibilityAnimation.setInterpolator(BakedBezierInterpolator.FADE_IN_CUR
VE); |
| 141 mTab.getWindowAndroid().startAnimationOverContent(mVisibilityAnimation); |
| 142 } |
| 143 |
| 144 /** Dismisses this results bar by removing it from the view hierarchy. */ |
| 145 public void dismiss() { |
| 146 mDismissing = true; |
| 147 if (mVisibilityAnimation != null && mVisibilityAnimation.isRunning()) { |
| 148 mVisibilityAnimation.cancel(); |
| 149 } |
| 150 |
| 151 mVisibilityAnimation = ObjectAnimator.ofFloat(this, TRANSLATION_X, |
| 152 MathUtils.flipSignIf(mBarTouchWidth, LocalizationUtils.isLayoutR
tl())); |
| 153 mVisibilityAnimation.setDuration(VISIBILTY_ANIMATION_DURATION_MS); |
| 154 mVisibilityAnimation.setInterpolator(BakedBezierInterpolator.FADE_OUT_CU
RVE); |
| 155 mTab.getWindowAndroid().startAnimationOverContent(mVisibilityAnimation); |
| 156 mVisibilityAnimation.addListener(new AnimatorListenerAdapter() { |
| 157 @Override |
| 158 public void onAnimationEnd(Animator animation) { |
| 159 super.onAnimationEnd(animation); |
| 160 |
| 161 if (getParent() != null) ((ViewGroup) getParent()).removeView(Fi
ndResultBar.this); |
| 162 } |
| 163 }); |
| 164 } |
| 165 |
| 166 /** Setup the tickmarks to draw using the rects of the find results. */ |
| 167 public void setMatchRects(int version, RectF[] rects, RectF activeRect) { |
| 168 if (mRectsVersion != version) { |
| 169 mRectsVersion = version; |
| 170 assert rects != null; |
| 171 mMatches = rects; |
| 172 mTickmarks.clear(); |
| 173 Arrays.sort(mMatches, sComparator); |
| 174 mBarHeightForWhichTickmarksWereCached = -1; |
| 175 } |
| 176 mActiveMatch = activeRect; // Can be null. |
| 177 invalidate(); |
| 178 } |
| 179 |
| 180 /** Clears the tickmarks. */ |
| 181 public void clearMatchRects() { |
| 182 setMatchRects(-1, new RectF[0], null); |
| 183 } |
| 184 |
| 185 @Override |
| 186 @SuppressLint("ClickableViewAccessibility") |
| 187 public boolean onTouchEvent(MotionEvent event) { |
| 188 if (!mDismissing && mTickmarks.size() > 0 && mTickmarks.size() == mMatch
es.length |
| 189 && !mWaitingForActivateAck && event.getAction() != MotionEvent.A
CTION_CANCEL) { |
| 190 // We decided it's more important to get the keyboard out of the |
| 191 // way asap; the user can compensate if their next MotionEvent |
| 192 // scrolls somewhere unintended. |
| 193 UiUtils.hideKeyboard(this); |
| 194 |
| 195 // Identify which drawn tickmark is closest to the user's finger. |
| 196 int closest = Collections.binarySearch(mTickmarks, |
| 197 new Tickmark(event.getY(), event.getY())); |
| 198 if (closest < 0) { |
| 199 // No exact match, so must determine nearest. |
| 200 int insertionPoint = -1 - closest; |
| 201 if (insertionPoint == 0) { |
| 202 closest = 0; |
| 203 } else if (insertionPoint == mTickmarks.size()) { |
| 204 closest = mTickmarks.size() - 1; |
| 205 } else { |
| 206 float distanceA = Math.abs(event.getY() |
| 207 - mTickmarks.get(insertionPoint - 1).centerY()); |
| 208 float distanceB = Math.abs(event.getY() |
| 209 - mTickmarks.get(insertionPoint).centerY()); |
| 210 closest = insertionPoint - (distanceA <= distanceB ? 1 : 0); |
| 211 } |
| 212 } |
| 213 |
| 214 // Now activate the find match corresponding to that tickmark. |
| 215 // Since mTickmarks may be outdated, we can't just pass the index. |
| 216 // Instead we send the renderer the coordinates of the center of the |
| 217 // find match's rect (as originally received in setMatchRects), and |
| 218 // it will activate whatever find result is currently closest to |
| 219 // that point (which will usually be the same one). |
| 220 mWaitingForActivateAck = true; |
| 221 mFindInPageBridge.activateNearestFindResult( |
| 222 mMatches[closest].centerX(), |
| 223 mMatches[closest].centerY()); |
| 224 } |
| 225 return true; // Consume the event, whether or not we acted upon it. |
| 226 } |
| 227 |
| 228 @Override |
| 229 protected void onSizeChanged(int w, int h, int oldw, int oldh) { |
| 230 super.onSizeChanged(w, h, oldw, oldh); |
| 231 // Check for new rects, as they may move if the document size changes. |
| 232 if (!mDismissing && mMatches.length > 0) { |
| 233 mFindInPageBridge.requestFindMatchRects(mRectsVersion); |
| 234 } |
| 235 } |
| 236 |
| 237 @Override |
| 238 protected void onDraw(Canvas canvas) { |
| 239 super.onDraw(canvas); |
| 240 |
| 241 int leftMargin = getLeftMargin(); |
| 242 mFillPaint.setColor(mBackgroundColor); |
| 243 mStrokePaint.setColor(mBackgroundBorderColor); |
| 244 canvas.drawRect(leftMargin, 0, |
| 245 leftMargin + mBarDrawWidth, getHeight(), mFillPaint); |
| 246 float lineX = LocalizationUtils.isLayoutRtl() |
| 247 ? leftMargin + mBarDrawWidth - 0.5f |
| 248 : leftMargin + 0.5f; |
| 249 canvas.drawLine(lineX, 0, lineX, getHeight(), mStrokePaint); |
| 250 |
| 251 if (mMatches.length == 0) { |
| 252 return; |
| 253 } |
| 254 |
| 255 if (mBarHeightForWhichTickmarksWereCached != getHeight()) { |
| 256 calculateTickmarks(); |
| 257 } |
| 258 |
| 259 // Draw all matches (since they're sorted by increasing y-position |
| 260 // overlapping tickmarks will form nice stacks). |
| 261 mFillPaint.setColor(mResultColor); |
| 262 mStrokePaint.setColor(mResultBorderColor); |
| 263 for (Tickmark tickmark : mTickmarks) { |
| 264 RectF rect = tickmark.toRectF(); |
| 265 canvas.drawRoundRect(rect, 2, 2, mFillPaint); |
| 266 canvas.drawRoundRect(rect, 2, 2, mStrokePaint); |
| 267 } |
| 268 |
| 269 // Draw the active tickmark on top (covering up the inactive tickmark |
| 270 // we probably already drew for it). |
| 271 if (mActiveMatch != null) { |
| 272 Tickmark tickmark; |
| 273 int i = Arrays.binarySearch(mMatches, mActiveMatch, sComparator); |
| 274 if (i >= 0) { |
| 275 // We've already generated a tickmark for all rects in mMatches, |
| 276 // so use the corresponding one. However it was generated |
| 277 // assuming the match would be inactive. Keep the position, but |
| 278 // re-expand it using mActiveMinHeight. |
| 279 tickmark = expandTickmarkToMinHeight(mTickmarks.get(i), true); |
| 280 } else { |
| 281 // How strange - mActiveMatch isn't in mMatches. Do our best to |
| 282 // draw it anyway (though it might not line up exactly). |
| 283 tickmark = tickmarkForRect(mActiveMatch, true); |
| 284 } |
| 285 RectF rect = tickmark.toRectF(); |
| 286 mFillPaint.setColor(mActiveColor); |
| 287 mStrokePaint.setColor(mActiveBorderColor); |
| 288 canvas.drawRoundRect(rect, 2, 2, mFillPaint); |
| 289 canvas.drawRoundRect(rect, 2, 2, mStrokePaint); |
| 290 } |
| 291 } |
| 292 |
| 293 private int getLeftMargin() { |
| 294 return LocalizationUtils.isLayoutRtl() ? 0 : getWidth() - mBarDrawWidth; |
| 295 } |
| 296 |
| 297 private void calculateTickmarks() { |
| 298 // TODO(johnme): Simplify calculation, and switch to integer arithmetic |
| 299 // where possible (tickmarks within groups will still need fractional |
| 300 // y-positions for anti-aliasing, but the start and end positions of |
| 301 // groups can and should be integer-aligned [will give crisp borders], |
| 302 // and the intermediary logic uses more floats than necessary). |
| 303 // TODO(johnme): Consider adding unit tests for this. |
| 304 |
| 305 mBarHeightForWhichTickmarksWereCached = getHeight(); |
| 306 |
| 307 // Generate tickmarks, neatly clustering any overlapping matches. |
| 308 mTickmarks = new ArrayList<Tickmark>(mMatches.length); |
| 309 int i = 0; |
| 310 Tickmark nextTickmark = tickmarkForRect(mMatches[i], false); |
| 311 float lastGroupEnd = -mMinGapBetweenStacks; |
| 312 while (i < mMatches.length) { |
| 313 // Find next cluster of overlapping tickmarks. |
| 314 List<Tickmark> cluster = new ArrayList<Tickmark>(); |
| 315 cluster.add(nextTickmark); |
| 316 i++; |
| 317 while (i < mMatches.length) { |
| 318 nextTickmark = tickmarkForRect(mMatches[i], false); |
| 319 if (nextTickmark.mTop <= cluster.get(cluster.size() - 1).mBottom |
| 320 + mMinGapBetweenStacks) { |
| 321 cluster.add(nextTickmark); |
| 322 i++; |
| 323 } else { |
| 324 break; |
| 325 } |
| 326 } |
| 327 |
| 328 // Draw cluster. |
| 329 int cn = cluster.size(); |
| 330 float minStart = lastGroupEnd + mMinGapBetweenStacks; |
| 331 lastGroupEnd = cluster.get(cn - 1).mBottom; |
| 332 float preferredStart = lastGroupEnd |
| 333 - (cn - 1) * mStackedResultHeight |
| 334 - mResultMinHeight; |
| 335 float maxStart = cluster.get(0).mTop; |
| 336 float start = Math.round(MathUtils.clamp(preferredStart, minStart, m
axStart)); |
| 337 float scale = start >= preferredStart ? 1.0f : |
| 338 (lastGroupEnd - start) / (lastGroupEnd - preferredStart); |
| 339 float spacing = cn == 1 ? 0 : (lastGroupEnd - start |
| 340 - scale * mResultMinHeight) / (cn - 1); |
| 341 for (int j = 0; j < cn; j++) { |
| 342 Tickmark tickmark = cluster.get(j); |
| 343 tickmark.mTop = start + j * spacing; |
| 344 if (j != cn - 1) { |
| 345 tickmark.mBottom = tickmark.mTop + scale * mResultMinHeight; |
| 346 } |
| 347 mTickmarks.add(tickmark); |
| 348 } |
| 349 } |
| 350 } |
| 351 |
| 352 private Tickmark tickmarkForRect(RectF r, boolean active) { |
| 353 // Ratio of results bar height to page height |
| 354 float vScale = mBarHeightForWhichTickmarksWereCached - 2 * mBarVerticalP
adding; |
| 355 Tickmark tickmark = new Tickmark( |
| 356 r.top * vScale + mBarVerticalPadding, |
| 357 r.bottom * vScale + mBarVerticalPadding); |
| 358 return expandTickmarkToMinHeight(tickmark, active); |
| 359 } |
| 360 |
| 361 private Tickmark expandTickmarkToMinHeight(Tickmark tickmark, |
| 362 boolean active) { |
| 363 int minHeight = active ? mActiveMinHeight : mResultMinHeight; |
| 364 float missingHeight = minHeight - tickmark.height(); |
| 365 if (missingHeight > 0) { |
| 366 return new Tickmark(tickmark.mTop - missingHeight / 2.0f, |
| 367 tickmark.mBottom + missingHeight / 2.0f); |
| 368 } |
| 369 return tickmark; |
| 370 } |
| 371 |
| 372 /** Like android.graphics.RectF, but without a left or right. */ |
| 373 private class Tickmark implements Comparable<Tickmark> { |
| 374 float mTop; |
| 375 float mBottom; |
| 376 Tickmark(float top, float bottom) { |
| 377 this.mTop = top; |
| 378 this.mBottom = bottom; |
| 379 } |
| 380 float height() { |
| 381 return mBottom - mTop; |
| 382 } |
| 383 float centerY() { |
| 384 return (mTop + mBottom) * 0.5f; |
| 385 } |
| 386 RectF toRectF() { |
| 387 int leftMargin = getLeftMargin(); |
| 388 RectF rect = new RectF(leftMargin, mTop, leftMargin + mBarDrawWidth,
mBottom); |
| 389 rect.inset(2.0f, 0.5f); |
| 390 rect.offset(LocalizationUtils.isLayoutRtl() ? -0.5f : 0.5f, 0); |
| 391 return rect; |
| 392 } |
| 393 @SuppressFBWarnings("EQ_COMPARETO_USE_OBJECT_EQUAL") |
| 394 @Override |
| 395 public int compareTo(Tickmark other) { |
| 396 float center = centerY(); |
| 397 float otherCenter = other.centerY(); |
| 398 if (center == otherCenter) return 0; |
| 399 return center > otherCenter ? 1 : -1; |
| 400 } |
| 401 } |
| 402 } |
OLD | NEW |