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.ntp; |
| 6 |
| 7 import android.annotation.SuppressLint; |
| 8 import android.content.Context; |
| 9 import android.content.res.Resources; |
| 10 import android.graphics.Bitmap; |
| 11 import android.graphics.Canvas; |
| 12 import android.graphics.Rect; |
| 13 import android.graphics.drawable.BitmapDrawable; |
| 14 import android.net.Uri; |
| 15 import android.os.Build; |
| 16 import android.support.v4.graphics.drawable.RoundedBitmapDrawable; |
| 17 import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; |
| 18 import android.text.Editable; |
| 19 import android.text.TextUtils; |
| 20 import android.text.TextWatcher; |
| 21 import android.text.method.LinkMovementMethod; |
| 22 import android.text.style.ClickableSpan; |
| 23 import android.util.AttributeSet; |
| 24 import android.view.LayoutInflater; |
| 25 import android.view.MotionEvent; |
| 26 import android.view.View; |
| 27 import android.view.View.OnLayoutChangeListener; |
| 28 import android.view.ViewGroup; |
| 29 import android.view.ViewStub; |
| 30 import android.widget.Button; |
| 31 import android.widget.FrameLayout; |
| 32 import android.widget.ImageView; |
| 33 import android.widget.TextView; |
| 34 |
| 35 import com.google.android.apps.chrome.R; |
| 36 |
| 37 import org.chromium.base.CommandLine; |
| 38 import org.chromium.base.FieldTrialList; |
| 39 import org.chromium.base.VisibleForTesting; |
| 40 import org.chromium.base.metrics.RecordHistogram; |
| 41 import org.chromium.chrome.browser.ApplicationSwitches; |
| 42 import org.chromium.chrome.browser.LogoBridge.Logo; |
| 43 import org.chromium.chrome.browser.LogoBridge.LogoObserver; |
| 44 import org.chromium.chrome.browser.favicon.FaviconHelper.FaviconImageCallback; |
| 45 import org.chromium.chrome.browser.favicon.LargeIconBridge.LargeIconCallback; |
| 46 import org.chromium.chrome.browser.ntp.MostVisitedItem.MostVisitedItemManager; |
| 47 import org.chromium.chrome.browser.ntp.NewTabPage.OnSearchBoxScrollListener; |
| 48 import org.chromium.chrome.browser.profiles.MostVisitedSites.MostVisitedURLsObse
rver; |
| 49 import org.chromium.chrome.browser.profiles.MostVisitedSites.ThumbnailCallback; |
| 50 import org.chromium.chrome.browser.util.FeatureUtilities; |
| 51 import org.chromium.chrome.browser.util.ViewUtils; |
| 52 import org.chromium.chrome.browser.widget.RoundedIconGenerator; |
| 53 import org.chromium.ui.text.SpanApplier; |
| 54 import org.chromium.ui.text.SpanApplier.SpanInfo; |
| 55 |
| 56 import java.util.Locale; |
| 57 |
| 58 /** |
| 59 * The native new tab page, represented by some basic data such as title and url
, and an Android |
| 60 * View that displays the page. |
| 61 */ |
| 62 public class NewTabPageView extends FrameLayout |
| 63 implements MostVisitedURLsObserver, OnLayoutChangeListener { |
| 64 |
| 65 static final int MAX_MOST_VISITED_SITES = 12; |
| 66 private static final int SHADOW_COLOR = 0x11000000; |
| 67 private static final long SNAP_SCROLL_DELAY_MS = 30; |
| 68 |
| 69 private static final String ICON_NTP_FIELD_TRIAL_NAME = "IconNTP"; |
| 70 private static final String ICON_NTP_ENABLED_GROUP = "Enabled"; |
| 71 |
| 72 private ViewGroup mContentView; |
| 73 private NewTabScrollView mScrollView; |
| 74 private LogoView mSearchProviderLogoView; |
| 75 private View mSearchBoxView; |
| 76 private TextView mSearchBoxTextView; |
| 77 private ImageView mVoiceSearchButton; |
| 78 private ViewGroup mMostVisitedLayout; |
| 79 private View mMostVisitedPlaceholder; |
| 80 private View mOptOutView; |
| 81 private View mNoSearchLogoSpacer; |
| 82 |
| 83 private OnSearchBoxScrollListener mSearchBoxScrollListener; |
| 84 |
| 85 private NewTabPageManager mManager; |
| 86 private MostVisitedDesign mMostVisitedDesign; |
| 87 private MostVisitedItem[] mMostVisitedItems; |
| 88 private boolean mFirstShow = true; |
| 89 private boolean mSearchProviderHasLogo = true; |
| 90 private boolean mHasReceivedMostVisitedSites; |
| 91 private boolean mPendingSnapScroll; |
| 92 |
| 93 /** |
| 94 * The number of asynchronous tasks that need to complete before the page is
done loading. |
| 95 * This starts at one to track when the view is finished attaching to the wi
ndow. |
| 96 */ |
| 97 private int mPendingLoadTasks = 1; |
| 98 private boolean mLoadHasCompleted; |
| 99 |
| 100 private float mUrlFocusChangePercent; |
| 101 private boolean mDisableUrlFocusChangeAnimations; |
| 102 |
| 103 private boolean mSnapshotMostVisitedChanged; |
| 104 private int mSnapshotWidth; |
| 105 private int mSnapshotHeight; |
| 106 private int mSnapshotScrollY; |
| 107 |
| 108 /** |
| 109 * Manages the view interaction with the rest of the system. |
| 110 */ |
| 111 public interface NewTabPageManager extends MostVisitedItemManager { |
| 112 /** @return Whether the location bar is shown in the NTP. */ |
| 113 boolean isLocationBarShownInNTP(); |
| 114 |
| 115 /** @return Whether the document mode opt out promo should be shown. */ |
| 116 boolean shouldShowOptOutPromo(); |
| 117 |
| 118 /** Called when the document mode opt out promo is shown. */ |
| 119 void optOutPromoShown(); |
| 120 |
| 121 /** Called when the user clicks "settings" or "ok, got it" on the opt ou
t promo. */ |
| 122 void optOutPromoClicked(boolean settingsClicked); |
| 123 |
| 124 /** Opens the bookmarks page in the current tab. */ |
| 125 void navigateToBookmarks(); |
| 126 |
| 127 /** Opens the recent tabs page in the current tab. */ |
| 128 void navigateToRecentTabs(); |
| 129 |
| 130 /** |
| 131 * Animates the search box up into the omnibox and bring up the keyboard
. |
| 132 * @param beginVoiceSearch Whether to begin a voice search. |
| 133 * @param pastedText Text to paste in the omnibox after it's been focuse
d. May be null. |
| 134 */ |
| 135 void focusSearchBox(boolean beginVoiceSearch, String pastedText); |
| 136 |
| 137 /** |
| 138 * Gets the list of most visited sites. |
| 139 * @param observer The observer to be notified with the list of sites. |
| 140 * @param numResults The maximum number of sites to retrieve. |
| 141 */ |
| 142 void setMostVisitedURLsObserver(MostVisitedURLsObserver observer, int nu
mResults); |
| 143 |
| 144 /** |
| 145 * Gets a cached thumbnail of a URL. |
| 146 * @param url The URL whose thumbnail is being retrieved. |
| 147 * @param thumbnailCallback The callback to be notified when the thumbna
il is available. |
| 148 */ |
| 149 void getURLThumbnail(String url, ThumbnailCallback thumbnailCallback); |
| 150 |
| 151 /** |
| 152 * Gets the favicon image for a given URL. |
| 153 * @param url The URL of the site whose favicon is being requested. |
| 154 * @param size The desired size of the favicon in pixels. |
| 155 * @param faviconCallback The callback to be notified when the favicon i
s available. |
| 156 */ |
| 157 void getLocalFaviconImageForURL( |
| 158 String url, int size, FaviconImageCallback faviconCallback); |
| 159 |
| 160 /** |
| 161 * Gets the large icon (e.g. favicon or touch icon) for a given URL. |
| 162 * @param url The URL of the site whose icon is being requested. |
| 163 * @param size The desired size of the icon in pixels. |
| 164 * @param callback The callback to be notified when the icon is availabl
e. |
| 165 */ |
| 166 void getLargeIconForUrl(String url, int size, LargeIconCallback callback
); |
| 167 |
| 168 /** |
| 169 * Navigates to a URL chosen by the search provider when the user clicks
on the logo. |
| 170 */ |
| 171 void openLogoLink(); |
| 172 |
| 173 /** |
| 174 * Gets the default search provider's logo and calls logoObserver with t
he result. |
| 175 * @param logoObserver The callback to notify when the logo is available
. |
| 176 */ |
| 177 void getSearchProviderLogo(LogoObserver logoObserver); |
| 178 |
| 179 /** |
| 180 * Called when the NTP has completely finished loading (all views will b
e inflated |
| 181 * and any dependent resources will have been loaded). |
| 182 */ |
| 183 void onLoadingComplete(); |
| 184 } |
| 185 |
| 186 /** |
| 187 * Returns a title suitable for display for a link (e.g. a most visited item
). If |title| is |
| 188 * non-empty, this simply returns it. Otherwise, returns a shortened form of
the URL. |
| 189 */ |
| 190 static String getTitleForDisplay(String title, String url) { |
| 191 if (TextUtils.isEmpty(title) && url != null) { |
| 192 Uri uri = Uri.parse(url); |
| 193 String host = uri.getHost(); |
| 194 String path = uri.getPath(); |
| 195 if (host == null) host = ""; |
| 196 if (TextUtils.isEmpty(path) || path.equals("/")) path = ""; |
| 197 title = host + path; |
| 198 } |
| 199 return title; |
| 200 } |
| 201 |
| 202 /** |
| 203 * Default constructor required for XML inflation. |
| 204 */ |
| 205 public NewTabPageView(Context context, AttributeSet attrs) { |
| 206 super(context, attrs); |
| 207 } |
| 208 |
| 209 private boolean isIconNtpEnabled() { |
| 210 // Query the field trial state first, to ensure that UMA reports the cor
rect group. |
| 211 String fieldTrialGroup = FieldTrialList.findFullName(ICON_NTP_FIELD_TRIA
L_NAME); |
| 212 CommandLine commandLine = CommandLine.getInstance(); |
| 213 if (commandLine.hasSwitch(ApplicationSwitches.DISABLE_ICON_NTP)) return
false; |
| 214 if (commandLine.hasSwitch(ApplicationSwitches.ENABLE_ICON_NTP)) return t
rue; |
| 215 return fieldTrialGroup.equals(ICON_NTP_ENABLED_GROUP); |
| 216 } |
| 217 |
| 218 /** |
| 219 * Initializes the NTP. This must be called immediately after inflation, bef
ore this object is |
| 220 * used in any other way. |
| 221 * |
| 222 * @param manager NewTabPageManager used to perform various actions when the
user interacts |
| 223 * with the page. |
| 224 * @param isSingleUrlBarMode Whether the NTP is in single URL bar mode. |
| 225 * @param searchProviderHasLogo Whether the search provider has a logo. |
| 226 */ |
| 227 public void initialize(NewTabPageManager manager, boolean isSingleUrlBarMode
, |
| 228 boolean searchProviderHasLogo) { |
| 229 mManager = manager; |
| 230 |
| 231 mScrollView = (NewTabScrollView) findViewById(R.id.ntp_scrollview); |
| 232 mScrollView.enableBottomShadow(SHADOW_COLOR); |
| 233 mContentView = (ViewGroup) findViewById(R.id.ntp_content); |
| 234 |
| 235 mMostVisitedDesign = isIconNtpEnabled() |
| 236 ? new IconMostVisitedDesign(getContext()) |
| 237 : new ThumbnailMostVisitedDesign(getContext()); |
| 238 ViewStub mostVisitedLayoutStub = (ViewStub) findViewById(R.id.most_visit
ed_layout_stub); |
| 239 mostVisitedLayoutStub.setLayoutResource(mMostVisitedDesign.getMostVisite
dLayoutId()); |
| 240 mMostVisitedLayout = (ViewGroup) mostVisitedLayoutStub.inflate(); |
| 241 |
| 242 mSearchProviderLogoView = (LogoView) findViewById(R.id.search_provider_l
ogo); |
| 243 mSearchBoxView = findViewById(R.id.search_box); |
| 244 mNoSearchLogoSpacer = findViewById(R.id.no_search_logo_spacer); |
| 245 |
| 246 mSearchBoxTextView = (TextView) mSearchBoxView.findViewById(R.id.search_
box_text); |
| 247 String hintText = getResources().getString(R.string.search_or_type_url); |
| 248 if (isSingleUrlBarMode) { |
| 249 mSearchBoxTextView.setHint(hintText); |
| 250 } else { |
| 251 mSearchBoxTextView.setContentDescription(hintText); |
| 252 } |
| 253 mSearchBoxTextView.setOnClickListener(new View.OnClickListener() { |
| 254 @Override |
| 255 public void onClick(View v) { |
| 256 mManager.focusSearchBox(false, null); |
| 257 } |
| 258 }); |
| 259 mSearchBoxTextView.addTextChangedListener(new TextWatcher() { |
| 260 @Override |
| 261 public void beforeTextChanged(CharSequence s, int start, int count,
int after) { |
| 262 } |
| 263 |
| 264 @Override |
| 265 public void onTextChanged(CharSequence s, int start, int before, int
count) { |
| 266 } |
| 267 |
| 268 @Override |
| 269 public void afterTextChanged(Editable s) { |
| 270 if (s.length() == 0) return; |
| 271 mManager.focusSearchBox(false, s.toString()); |
| 272 mSearchBoxTextView.setText(""); |
| 273 } |
| 274 }); |
| 275 |
| 276 mVoiceSearchButton = (ImageView) findViewById(R.id.voice_search_button); |
| 277 mVoiceSearchButton.setOnClickListener(new View.OnClickListener() { |
| 278 @Override |
| 279 public void onClick(View v) { |
| 280 mManager.focusSearchBox(true, null); |
| 281 } |
| 282 }); |
| 283 |
| 284 NewTabPageToolbar toolbar = (NewTabPageToolbar) findViewById(R.id.ntp_to
olbar); |
| 285 toolbar.getRecentTabsButton().setOnClickListener(new View.OnClickListene
r() { |
| 286 @Override |
| 287 public void onClick(View v) { |
| 288 mManager.navigateToRecentTabs(); |
| 289 } |
| 290 }); |
| 291 toolbar.getBookmarksButton().setOnClickListener(new View.OnClickListener
() { |
| 292 @Override |
| 293 public void onClick(View v) { |
| 294 mManager.navigateToBookmarks(); |
| 295 } |
| 296 }); |
| 297 |
| 298 initializeSearchBoxScrollHandling(); |
| 299 addOnLayoutChangeListener(this); |
| 300 setSearchProviderHasLogo(searchProviderHasLogo); |
| 301 |
| 302 mPendingLoadTasks++; |
| 303 mManager.setMostVisitedURLsObserver(this, |
| 304 mMostVisitedDesign.getNumberOfTiles(searchProviderHasLogo)); |
| 305 |
| 306 if (mManager.shouldShowOptOutPromo()) showOptOutPromo(); |
| 307 } |
| 308 |
| 309 private int getTabsMovedIllustration() { |
| 310 switch (Build.MANUFACTURER.toLowerCase(Locale.US)) { |
| 311 case "samsung": |
| 312 return R.drawable.tabs_moved_samsung; |
| 313 case "htc": |
| 314 return R.drawable.tabs_moved_htc; |
| 315 default: |
| 316 return R.drawable.tabs_moved_nexus; |
| 317 } |
| 318 } |
| 319 |
| 320 private void showOptOutPromo() { |
| 321 ViewStub optOutPromoStub = (ViewStub) findViewById(R.id.opt_out_promo_st
ub); |
| 322 mOptOutView = optOutPromoStub.inflate(); |
| 323 // Fill in opt-out text with Settings link |
| 324 TextView optOutText = (TextView) mOptOutView.findViewById(R.id.opt_out_t
ext); |
| 325 |
| 326 ClickableSpan settingsLink = new ClickableSpan() { |
| 327 @Override |
| 328 public void onClick(View view) { |
| 329 mManager.optOutPromoClicked(true); |
| 330 } |
| 331 |
| 332 // Change link formatting to use our blue control color and no under
line |
| 333 @Override |
| 334 public void updateDrawState(android.text.TextPaint textPaint) { |
| 335 textPaint.setColor(getContext().getResources().getColor( |
| 336 R.color.light_active_color)); |
| 337 textPaint.setUnderlineText(false); |
| 338 } |
| 339 }; |
| 340 |
| 341 optOutText.setText(SpanApplier.applySpans( |
| 342 getContext().getString(R.string.tabs_and_apps_opt_out_text), |
| 343 new SpanInfo("<link>", "</link>", settingsLink))); |
| 344 optOutText.setMovementMethod(LinkMovementMethod.getInstance()); |
| 345 |
| 346 ImageView illustration = (ImageView) mOptOutView.findViewById(R.id.tabs_
moved_illustration); |
| 347 illustration.setImageResource(getTabsMovedIllustration()); |
| 348 |
| 349 mOptOutView.setVisibility(View.VISIBLE); |
| 350 mMostVisitedLayout.setVisibility(View.GONE); |
| 351 |
| 352 Button gotItButton = (Button) mOptOutView.findViewById(R.id.got_it_butto
n); |
| 353 gotItButton.setOnClickListener(new View.OnClickListener() { |
| 354 @Override |
| 355 public void onClick(View v) { |
| 356 mOptOutView.setVisibility(View.GONE); |
| 357 mMostVisitedLayout.setVisibility(View.VISIBLE); |
| 358 mManager.optOutPromoClicked(false); |
| 359 updateMostVisitedPlaceholderVisibility(); |
| 360 } |
| 361 }); |
| 362 mManager.optOutPromoShown(); |
| 363 } |
| 364 |
| 365 private void updateSearchBoxOnScroll() { |
| 366 if (mDisableUrlFocusChangeAnimations) return; |
| 367 |
| 368 float percentage = 0; |
| 369 // During startup the view may not be fully initialized, so we only calc
ulate the current |
| 370 // percentage if some basic view properties are sane. |
| 371 if (mScrollView.getHeight() != 0 && mSearchBoxView.getTop() != 0) { |
| 372 int scrollY = mScrollView.getScrollY(); |
| 373 percentage = Math.max( |
| 374 0f, Math.min(1f, scrollY / (float) mSearchBoxView.getTop()))
; |
| 375 } |
| 376 |
| 377 updateVisualsForToolbarTransition(percentage); |
| 378 |
| 379 if (mSearchBoxScrollListener != null) { |
| 380 mSearchBoxScrollListener.onScrollChanged(percentage); |
| 381 } |
| 382 } |
| 383 |
| 384 private void initializeSearchBoxScrollHandling() { |
| 385 final Runnable mSnapScrollRunnable = new Runnable() { |
| 386 @Override |
| 387 public void run() { |
| 388 if (!mPendingSnapScroll) return; |
| 389 int scrollY = mScrollView.getScrollY(); |
| 390 int dividerTop = mMostVisitedLayout.getTop() - mContentView.getP
addingTop(); |
| 391 if (scrollY > 0 && scrollY < dividerTop) { |
| 392 mScrollView.smoothScrollTo(0, scrollY < (dividerTop / 2) ? 0
: dividerTop); |
| 393 } |
| 394 mPendingSnapScroll = false; |
| 395 } |
| 396 }; |
| 397 mScrollView.setOnScrollListener(new NewTabScrollView.OnScrollListener()
{ |
| 398 @Override |
| 399 public void onScrollChanged(int l, int t, int oldl, int oldt) { |
| 400 if (mPendingSnapScroll) { |
| 401 mScrollView.removeCallbacks(mSnapScrollRunnable); |
| 402 mScrollView.postDelayed(mSnapScrollRunnable, SNAP_SCROLL_DEL
AY_MS); |
| 403 } |
| 404 updateSearchBoxOnScroll(); |
| 405 } |
| 406 }); |
| 407 mScrollView.setOnTouchListener(new OnTouchListener() { |
| 408 @Override |
| 409 @SuppressLint("ClickableViewAccessibility") |
| 410 public boolean onTouch(View v, MotionEvent event) { |
| 411 if (mScrollView.getHandler() == null) return false; |
| 412 mScrollView.removeCallbacks(mSnapScrollRunnable); |
| 413 |
| 414 if (event.getActionMasked() == MotionEvent.ACTION_CANCEL |
| 415 || event.getActionMasked() == MotionEvent.ACTION_UP) { |
| 416 mPendingSnapScroll = true; |
| 417 mScrollView.postDelayed(mSnapScrollRunnable, SNAP_SCROLL_DEL
AY_MS); |
| 418 } else { |
| 419 mPendingSnapScroll = false; |
| 420 } |
| 421 return false; |
| 422 } |
| 423 }); |
| 424 } |
| 425 |
| 426 /** |
| 427 * Decrements the count of pending load tasks and notifies the manager when
the page load |
| 428 * is complete. |
| 429 */ |
| 430 private void loadTaskCompleted() { |
| 431 assert mPendingLoadTasks > 0; |
| 432 mPendingLoadTasks--; |
| 433 if (mPendingLoadTasks == 0) { |
| 434 if (mLoadHasCompleted) { |
| 435 assert false; |
| 436 } else { |
| 437 mLoadHasCompleted = true; |
| 438 mManager.onLoadingComplete(); |
| 439 mMostVisitedDesign.onLoadingComplete(); |
| 440 // Load the logo after everything else is finished, since it's l
ower priority. |
| 441 loadSearchProviderLogo(); |
| 442 } |
| 443 } |
| 444 } |
| 445 |
| 446 /** |
| 447 * Loads the search provider logo (e.g. Google doodle), if any. |
| 448 */ |
| 449 private void loadSearchProviderLogo() { |
| 450 mManager.getSearchProviderLogo(new LogoObserver() { |
| 451 @Override |
| 452 public void onLogoAvailable(Logo logo, boolean fromCache) { |
| 453 if (logo == null && fromCache) return; |
| 454 mSearchProviderLogoView.setMananger(mManager); |
| 455 mSearchProviderLogoView.updateLogo(logo); |
| 456 mSnapshotMostVisitedChanged = true; |
| 457 } |
| 458 }); |
| 459 } |
| 460 |
| 461 /** |
| 462 * Changes the layout depending on whether the selected search provider (e.g
. Google, Bing) |
| 463 * has a logo. |
| 464 * @param hasLogo Whether the search provider has a logo. |
| 465 */ |
| 466 public void setSearchProviderHasLogo(boolean hasLogo) { |
| 467 if (hasLogo == mSearchProviderHasLogo) return; |
| 468 mSearchProviderHasLogo = hasLogo; |
| 469 |
| 470 mMostVisitedDesign.setSearchProviderHasLogo(mMostVisitedLayout, hasLogo)
; |
| 471 |
| 472 if (!hasLogo) setUrlFocusChangeAnimationPercentInternal(0); |
| 473 |
| 474 // Hide or show all the views above the Most Visited items. |
| 475 int visibility = hasLogo ? View.VISIBLE : View.GONE; |
| 476 int childCount = mContentView.getChildCount(); |
| 477 for (int i = 0; i < childCount; i++) { |
| 478 View child = mContentView.getChildAt(i); |
| 479 if (child == mMostVisitedLayout) break; |
| 480 // Don't change the visibility of a ViewStub as that will automagica
lly inflate it. |
| 481 if (child instanceof ViewStub) continue; |
| 482 child.setVisibility(visibility); |
| 483 } |
| 484 |
| 485 updateMostVisitedPlaceholderVisibility(); |
| 486 |
| 487 if (hasLogo) setUrlFocusChangeAnimationPercent(mUrlFocusChangePercent); |
| 488 mSnapshotMostVisitedChanged = true; |
| 489 } |
| 490 |
| 491 /** |
| 492 * Updates whether the NewTabPage should animate on URL focus changes. |
| 493 * @param disable Whether to disable the animations. |
| 494 */ |
| 495 void setUrlFocusAnimationsDisabled(boolean disable) { |
| 496 if (disable == mDisableUrlFocusChangeAnimations) return; |
| 497 mDisableUrlFocusChangeAnimations = disable; |
| 498 if (!disable) setUrlFocusChangeAnimationPercent(mUrlFocusChangePercent); |
| 499 } |
| 500 |
| 501 /** |
| 502 * @return Whether URL focus animations are currently disabled. |
| 503 */ |
| 504 boolean urlFocusAnimationsDisabled() { |
| 505 return mDisableUrlFocusChangeAnimations; |
| 506 } |
| 507 |
| 508 /** |
| 509 * Specifies the percentage the URL is focused during an animation. 1.0 spe
cifies that the URL |
| 510 * bar has focus and has completed the focus animation. 0 is when the URL b
ar is does not have |
| 511 * any focus. |
| 512 * |
| 513 * @param percent The percentage of the URL bar focus animation. |
| 514 */ |
| 515 void setUrlFocusChangeAnimationPercent(float percent) { |
| 516 mUrlFocusChangePercent = percent; |
| 517 if (!mDisableUrlFocusChangeAnimations && mSearchProviderHasLogo) { |
| 518 setUrlFocusChangeAnimationPercentInternal(percent); |
| 519 } |
| 520 } |
| 521 |
| 522 /** |
| 523 * @return The percentage that the URL bar is focused during an animation. |
| 524 */ |
| 525 @VisibleForTesting |
| 526 float getUrlFocusChangeAnimationPercent() { |
| 527 return mUrlFocusChangePercent; |
| 528 } |
| 529 |
| 530 /** |
| 531 * Unconditionally sets the percentage the URL is focused during an animatio
n, without updating |
| 532 * mUrlFocusChangePercent. |
| 533 * @see #setUrlFocusChangeAnimationPercent |
| 534 */ |
| 535 private void setUrlFocusChangeAnimationPercentInternal(float percent) { |
| 536 mContentView.setTranslationY(percent * (-mMostVisitedLayout.getTop() |
| 537 + mScrollView.getScrollY() + mContentView.getPaddingTop())); |
| 538 updateVisualsForToolbarTransition(percent); |
| 539 } |
| 540 |
| 541 private void updateVisualsForToolbarTransition(float transitionPercentage) { |
| 542 // Complete the full alpha transition in the first 40% of the animation. |
| 543 float searchUiAlpha = |
| 544 transitionPercentage >= 0.4f ? 0f : (0.4f - transitionPercentage
) * 2.5f; |
| 545 // Ensure there are no rounding issues when the animation percent is 0. |
| 546 if (transitionPercentage == 0f) searchUiAlpha = 1f; |
| 547 |
| 548 mSearchProviderLogoView.setAlpha(searchUiAlpha); |
| 549 mSearchBoxView.setAlpha(searchUiAlpha); |
| 550 } |
| 551 |
| 552 /** |
| 553 * Get the bounds of the search box in relation to the top level NewTabPage
view. |
| 554 * |
| 555 * @param originalBounds The bounding region of the search box without exter
nal transforms |
| 556 * applied. The delta between this and the transforme
d bounds determines |
| 557 * the amount of scroll applied to this view. |
| 558 * @param transformedBounds The bounding region of the search box including
any transforms |
| 559 * applied by the parent view hierarchy up to the N
ewTabPage view. |
| 560 * This more accurately reflects the current drawin
g location of the |
| 561 * search box. |
| 562 */ |
| 563 void getSearchBoxBounds(Rect originalBounds, Rect transformedBounds) { |
| 564 int searchBoxX = (int) mSearchBoxView.getX(); |
| 565 int searchBoxY = (int) mSearchBoxView.getY(); |
| 566 originalBounds.set( |
| 567 searchBoxX + mSearchBoxView.getPaddingLeft(), |
| 568 searchBoxY + mSearchBoxView.getPaddingTop(), |
| 569 searchBoxX + mSearchBoxView.getWidth() - mSearchBoxView.getPaddi
ngRight(), |
| 570 searchBoxY + mSearchBoxView.getHeight() - mSearchBoxView.getPadd
ingBottom()); |
| 571 |
| 572 transformedBounds.set(originalBounds); |
| 573 View view = (View) mSearchBoxView.getParent(); |
| 574 while (view != null) { |
| 575 transformedBounds.offset(-view.getScrollX(), -view.getScrollY()); |
| 576 if (view == this) break; |
| 577 transformedBounds.offset((int) view.getX(), (int) view.getY()); |
| 578 view = (View) view.getParent(); |
| 579 } |
| 580 } |
| 581 |
| 582 /** |
| 583 * Sets the listener for search box scroll changes. |
| 584 * @param listener The listener to be notified on changes. |
| 585 */ |
| 586 void setSearchBoxScrollListener(OnSearchBoxScrollListener listener) { |
| 587 mSearchBoxScrollListener = listener; |
| 588 if (mSearchBoxScrollListener != null) updateSearchBoxOnScroll(); |
| 589 } |
| 590 |
| 591 @Override |
| 592 protected void onAttachedToWindow() { |
| 593 super.onAttachedToWindow(); |
| 594 assert mManager != null; |
| 595 |
| 596 if (mFirstShow) { |
| 597 loadTaskCompleted(); |
| 598 mFirstShow = false; |
| 599 } else { |
| 600 // Trigger a scroll update when reattaching the window to signal the
toolbar that |
| 601 // it needs to reset the NTP state. |
| 602 if (mManager.isLocationBarShownInNTP()) updateSearchBoxOnScroll(); |
| 603 } |
| 604 } |
| 605 |
| 606 @Override |
| 607 protected void onDetachedFromWindow() { |
| 608 super.onDetachedFromWindow(); |
| 609 setUrlFocusChangeAnimationPercent(0f); |
| 610 } |
| 611 |
| 612 @Override |
| 613 protected void onWindowVisibilityChanged(int visibility) { |
| 614 super.onWindowVisibilityChanged(visibility); |
| 615 |
| 616 if (visibility == VISIBLE) { |
| 617 mVoiceSearchButton.setVisibility( |
| 618 FeatureUtilities.isRecognitionIntentPresent(getContext(), tr
ue) |
| 619 ? VISIBLE : GONE); |
| 620 } |
| 621 } |
| 622 |
| 623 @Override |
| 624 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| 625 super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| 626 |
| 627 // Make the search box and logo the same width as the most visited tiles
. |
| 628 if (mMostVisitedLayout.getVisibility() != GONE) { |
| 629 int mostVisitedWidth = MeasureSpec.makeMeasureSpec( |
| 630 mMostVisitedLayout.getMeasuredWidth(), MeasureSpec.EXACTLY); |
| 631 int searchBoxHeight = MeasureSpec.makeMeasureSpec( |
| 632 mSearchBoxView.getMeasuredHeight(), MeasureSpec.EXACTLY); |
| 633 int logoHeight = MeasureSpec.makeMeasureSpec( |
| 634 mSearchProviderLogoView.getMeasuredHeight(), MeasureSpec.EXA
CTLY); |
| 635 mSearchBoxView.measure(mostVisitedWidth, searchBoxHeight); |
| 636 mSearchProviderLogoView.measure(mostVisitedWidth, logoHeight); |
| 637 } |
| 638 } |
| 639 |
| 640 /** |
| 641 * @see org.chromium.chrome.browser.compositor.layouts.content. |
| 642 * InvalidationAwareThumbnailProvider#shouldCaptureThumbnail() |
| 643 */ |
| 644 boolean shouldCaptureThumbnail() { |
| 645 if (getWidth() == 0 || getHeight() == 0) return false; |
| 646 |
| 647 return mSnapshotMostVisitedChanged |
| 648 || getWidth() != mSnapshotWidth |
| 649 || getHeight() != mSnapshotHeight |
| 650 || mScrollView.getScrollY() != mSnapshotScrollY; |
| 651 } |
| 652 |
| 653 /** |
| 654 * @see org.chromium.chrome.browser.compositor.layouts.content. |
| 655 * InvalidationAwareThumbnailProvider#captureThumbnail(Canvas) |
| 656 */ |
| 657 void captureThumbnail(Canvas canvas) { |
| 658 mSearchProviderLogoView.endAnimation(); |
| 659 ViewUtils.captureBitmap(this, canvas); |
| 660 mSnapshotWidth = getWidth(); |
| 661 mSnapshotHeight = getHeight(); |
| 662 mSnapshotScrollY = mScrollView.getScrollY(); |
| 663 mSnapshotMostVisitedChanged = false; |
| 664 } |
| 665 |
| 666 // OnLayoutChangeListener overrides |
| 667 |
| 668 @Override |
| 669 public void onLayoutChange(View v, int left, int top, int right, int bottom, |
| 670 int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| 671 int oldWidth = oldRight - oldLeft; |
| 672 int newWidth = right - left; |
| 673 if (oldWidth == newWidth) return; |
| 674 |
| 675 // Re-apply the url focus change amount after a rotation to ensure the v
iews are correctly |
| 676 // placed with their new layout configurations. |
| 677 setUrlFocusChangeAnimationPercent(mUrlFocusChangePercent); |
| 678 } |
| 679 |
| 680 // MostVisitedURLsObserver implementation |
| 681 |
| 682 @Override |
| 683 public void onMostVisitedURLsAvailable(String[] titles, String[] urls) { |
| 684 mMostVisitedLayout.removeAllViews(); |
| 685 |
| 686 MostVisitedItem[] oldItems = mMostVisitedItems; |
| 687 int oldItemCount = oldItems == null ? 0 : oldItems.length; |
| 688 mMostVisitedItems = new MostVisitedItem[titles.length]; |
| 689 |
| 690 final boolean isInitialLoad = !mHasReceivedMostVisitedSites; |
| 691 LayoutInflater inflater = LayoutInflater.from(getContext()); |
| 692 |
| 693 // Add the most visited items to the page. |
| 694 for (int i = 0; i < titles.length; i++) { |
| 695 final String url = urls[i]; |
| 696 final String title = titles[i]; |
| 697 |
| 698 // Look for an existing item to reuse. |
| 699 MostVisitedItem item = null; |
| 700 for (int j = 0; j < oldItemCount; j++) { |
| 701 MostVisitedItem oldItem = oldItems[j]; |
| 702 if (oldItem != null && TextUtils.equals(url, oldItem.getUrl()) |
| 703 && TextUtils.equals(title, oldItem.getTitle())) { |
| 704 item = oldItem; |
| 705 item.setIndex(i); |
| 706 oldItems[j] = null; |
| 707 break; |
| 708 } |
| 709 } |
| 710 |
| 711 // If nothing can be reused, create a new item. |
| 712 if (item == null) { |
| 713 String displayTitle = getTitleForDisplay(title, url); |
| 714 View view = mMostVisitedDesign.createMostVisitedItemView(inflate
r, url, title, |
| 715 displayTitle, i, isInitialLoad); |
| 716 item = new MostVisitedItem(mManager, title, url, i, view); |
| 717 } |
| 718 |
| 719 mMostVisitedItems[i] = item; |
| 720 mMostVisitedLayout.addView(item.getView()); |
| 721 } |
| 722 |
| 723 mHasReceivedMostVisitedSites = true; |
| 724 updateMostVisitedPlaceholderVisibility(); |
| 725 |
| 726 if (isInitialLoad) { |
| 727 loadTaskCompleted(); |
| 728 // The page contents are initially hidden; otherwise they'll be draw
n centered on the |
| 729 // page before the most visited sites are available and then jump up
wards to make space |
| 730 // once the most visited sites are available. |
| 731 mContentView.setVisibility(View.VISIBLE); |
| 732 } |
| 733 mSnapshotMostVisitedChanged = true; |
| 734 } |
| 735 |
| 736 /** |
| 737 * Shows the most visited placeholder ("Nothing to see here") if there are n
o most visited |
| 738 * items and there is no search provider logo. |
| 739 */ |
| 740 private void updateMostVisitedPlaceholderVisibility() { |
| 741 boolean showPlaceholder = mHasReceivedMostVisitedSites |
| 742 && !mManager.shouldShowOptOutPromo() |
| 743 && mMostVisitedLayout.getChildCount() == 0 |
| 744 && !mSearchProviderHasLogo; |
| 745 |
| 746 mNoSearchLogoSpacer.setVisibility( |
| 747 (mSearchProviderHasLogo || showPlaceholder) ? View.GONE : View.I
NVISIBLE); |
| 748 |
| 749 if (showPlaceholder) { |
| 750 if (mMostVisitedPlaceholder == null) { |
| 751 ViewStub mostVisitedPlaceholderStub = (ViewStub) findViewById( |
| 752 R.id.most_visited_placeholder_stub); |
| 753 mMostVisitedPlaceholder = mostVisitedPlaceholderStub.inflate(); |
| 754 } |
| 755 mMostVisitedLayout.setVisibility(GONE); |
| 756 mMostVisitedPlaceholder.setVisibility(VISIBLE); |
| 757 return; |
| 758 } else if (mMostVisitedPlaceholder != null) { |
| 759 mMostVisitedLayout.setVisibility(VISIBLE); |
| 760 mMostVisitedPlaceholder.setVisibility(GONE); |
| 761 } |
| 762 } |
| 763 |
| 764 /** |
| 765 * Interface for creating the most visited layout and tiles. |
| 766 * TODO(newt): delete this once a single design has been chosen. |
| 767 */ |
| 768 private interface MostVisitedDesign { |
| 769 int getNumberOfTiles(boolean searchProviderHasLogo); |
| 770 int getMostVisitedLayoutId(); |
| 771 void setSearchProviderHasLogo(View mostVisitedLayout, boolean hasLogo); |
| 772 View createMostVisitedItemView(LayoutInflater inflater, String url, Stri
ng title, |
| 773 String displayTitle, int index, boolean isInitialLoad); |
| 774 void onLoadingComplete(); |
| 775 } |
| 776 |
| 777 /** |
| 778 * The old most visited design, where each tile shows a thumbnail of the pag
e, a small favicon, |
| 779 * and the title. |
| 780 */ |
| 781 private class ThumbnailMostVisitedDesign implements MostVisitedDesign { |
| 782 |
| 783 private static final int NUM_TILES = 6; |
| 784 private static final int FAVICON_CORNER_RADIUS_DP = 2; |
| 785 private static final int FAVICON_TEXT_SIZE_DP = 10; |
| 786 private static final int FAVICON_BACKGROUND_COLOR = 0xff969696; |
| 787 |
| 788 private int mDesiredFaviconSize; |
| 789 private RoundedIconGenerator mFaviconGenerator; |
| 790 |
| 791 ThumbnailMostVisitedDesign(Context context) { |
| 792 Resources res = context.getResources(); |
| 793 mDesiredFaviconSize = res.getDimensionPixelSize(R.dimen.most_visited
_favicon_size); |
| 794 int desiredFaviconSizeDp = Math.round( |
| 795 mDesiredFaviconSize / res.getDisplayMetrics().density); |
| 796 mFaviconGenerator = new RoundedIconGenerator( |
| 797 context, desiredFaviconSizeDp, desiredFaviconSizeDp, FAVICON
_CORNER_RADIUS_DP, |
| 798 FAVICON_BACKGROUND_COLOR, FAVICON_TEXT_SIZE_DP); |
| 799 } |
| 800 |
| 801 @Override |
| 802 public int getNumberOfTiles(boolean searchProviderHasLogo) { |
| 803 return NUM_TILES; |
| 804 } |
| 805 |
| 806 @Override |
| 807 public int getMostVisitedLayoutId() { |
| 808 return R.layout.most_visited_layout; |
| 809 } |
| 810 |
| 811 @Override |
| 812 public void setSearchProviderHasLogo(View mostVisitedLayout, boolean has
Logo) {} |
| 813 |
| 814 @Override |
| 815 public View createMostVisitedItemView(LayoutInflater inflater, final Str
ing url, |
| 816 String title, String displayTitle, int index, final boolean isIn
itialLoad) { |
| 817 final MostVisitedItemView view = (MostVisitedItemView) inflater.infl
ate( |
| 818 R.layout.most_visited_item, mMostVisitedLayout, false); |
| 819 view.init(displayTitle); |
| 820 |
| 821 ThumbnailCallback thumbnailCallback = new ThumbnailCallback() { |
| 822 @Override |
| 823 public void onMostVisitedURLsThumbnailAvailable(Bitmap thumbnail
) { |
| 824 view.setThumbnail(thumbnail); |
| 825 mSnapshotMostVisitedChanged = true; |
| 826 if (isInitialLoad) loadTaskCompleted(); |
| 827 } |
| 828 }; |
| 829 if (isInitialLoad) mPendingLoadTasks++; |
| 830 mManager.getURLThumbnail(url, thumbnailCallback); |
| 831 |
| 832 FaviconImageCallback faviconCallback = new FaviconImageCallback() { |
| 833 @Override |
| 834 public void onFaviconAvailable(Bitmap image, String iconUrl) { |
| 835 if (image == null) { |
| 836 image = mFaviconGenerator.generateIconForUrl(url); |
| 837 } |
| 838 view.setFavicon(image); |
| 839 mSnapshotMostVisitedChanged = true; |
| 840 if (isInitialLoad) loadTaskCompleted(); |
| 841 } |
| 842 }; |
| 843 if (isInitialLoad) mPendingLoadTasks++; |
| 844 mManager.getLocalFaviconImageForURL(url, mDesiredFaviconSize, favico
nCallback); |
| 845 |
| 846 return view; |
| 847 } |
| 848 |
| 849 @Override |
| 850 public void onLoadingComplete() {} |
| 851 } |
| 852 |
| 853 /** |
| 854 * The new-fangled design for most visited tiles, where each tile shows a la
rge icon and title. |
| 855 */ |
| 856 private class IconMostVisitedDesign implements MostVisitedDesign { |
| 857 |
| 858 private static final int NUM_TILES = 8; |
| 859 private static final int NUM_TILES_NO_LOGO = 12; |
| 860 |
| 861 private static final int ICON_CORNER_RADIUS_DP = 4; |
| 862 private static final int ICON_TEXT_SIZE_DP = 20; |
| 863 private static final int ICON_BACKGROUND_COLOR = 0xff787878; |
| 864 private static final int ICON_MIN_SIZE_PX = 48; |
| 865 |
| 866 private int mMinIconSize; |
| 867 private int mDesiredIconSize; |
| 868 private RoundedIconGenerator mIconGenerator; |
| 869 |
| 870 private int mNumGrayIcons; |
| 871 private int mNumColorIcons; |
| 872 private int mNumRealIcons; |
| 873 |
| 874 IconMostVisitedDesign(Context context) { |
| 875 Resources res = context.getResources(); |
| 876 mDesiredIconSize = res.getDimensionPixelSize(R.dimen.icon_most_visit
ed_icon_size); |
| 877 // On ldpi devices, mDesiredIconSize could be even smaller than ICON
_MIN_SIZE_PX. |
| 878 mMinIconSize = Math.min(mDesiredIconSize, ICON_MIN_SIZE_PX); |
| 879 int desiredIconSizeDp = Math.round( |
| 880 mDesiredIconSize / res.getDisplayMetrics().density); |
| 881 mIconGenerator = new RoundedIconGenerator( |
| 882 context, desiredIconSizeDp, desiredIconSizeDp, ICON_CORNER_R
ADIUS_DP, |
| 883 ICON_BACKGROUND_COLOR, ICON_TEXT_SIZE_DP); |
| 884 } |
| 885 |
| 886 @Override |
| 887 public int getNumberOfTiles(boolean searchProviderHasLogo) { |
| 888 return searchProviderHasLogo ? NUM_TILES : NUM_TILES_NO_LOGO; |
| 889 } |
| 890 |
| 891 @Override |
| 892 public int getMostVisitedLayoutId() { |
| 893 return R.layout.icon_most_visited_layout; |
| 894 } |
| 895 |
| 896 @Override |
| 897 public void setSearchProviderHasLogo(View mostVisitedLayout, boolean has
Logo) { |
| 898 if (hasLogo) { |
| 899 int paddingTop = getResources().getDimensionPixelSize( |
| 900 R.dimen.icon_most_visited_layout_padding_top); |
| 901 int paddingSide = getResources().getDimensionPixelSize( |
| 902 R.dimen.icon_most_visited_layout_padding_side); |
| 903 mostVisitedLayout.setPadding(paddingSide, paddingTop, paddingSid
e, 0); |
| 904 } else { |
| 905 int paddingTop = getResources().getDimensionPixelSize( |
| 906 R.dimen.icon_most_visited_layout_no_logo_padding_top); |
| 907 mostVisitedLayout.setPadding(0, paddingTop, 0, 0); |
| 908 } |
| 909 } |
| 910 |
| 911 @Override |
| 912 public View createMostVisitedItemView(LayoutInflater inflater, final Str
ing url, |
| 913 String title, String displayTitle, int index, final boolean isIn
itialLoad) { |
| 914 final IconMostVisitedItemView view = (IconMostVisitedItemView) infla
ter.inflate( |
| 915 R.layout.icon_most_visited_item, mMostVisitedLayout, false); |
| 916 view.setTitle(displayTitle); |
| 917 |
| 918 LargeIconCallback iconCallback = new LargeIconCallback() { |
| 919 @Override |
| 920 public void onLargeIconAvailable(Bitmap icon, int fallbackColor)
{ |
| 921 if (icon == null) { |
| 922 mIconGenerator.setBackgroundColor(fallbackColor); |
| 923 icon = mIconGenerator.generateIconForUrl(url); |
| 924 view.setIcon(new BitmapDrawable(getResources(), icon)); |
| 925 if (isInitialLoad) { |
| 926 if (fallbackColor == ICON_BACKGROUND_COLOR) { |
| 927 mNumGrayIcons++; |
| 928 } else { |
| 929 mNumColorIcons++; |
| 930 } |
| 931 } |
| 932 } else { |
| 933 RoundedBitmapDrawable roundedIcon = RoundedBitmapDrawabl
eFactory.create( |
| 934 getResources(), icon); |
| 935 int cornerRadius = Math.round(ICON_CORNER_RADIUS_DP |
| 936 * getResources().getDisplayMetrics().density * i
con.getWidth() |
| 937 / mDesiredIconSize); |
| 938 roundedIcon.setCornerRadius(cornerRadius); |
| 939 roundedIcon.setAntiAlias(true); |
| 940 roundedIcon.setFilterBitmap(true); |
| 941 view.setIcon(roundedIcon); |
| 942 if (isInitialLoad) mNumRealIcons++; |
| 943 } |
| 944 mSnapshotMostVisitedChanged = true; |
| 945 if (isInitialLoad) loadTaskCompleted(); |
| 946 } |
| 947 }; |
| 948 if (isInitialLoad) mPendingLoadTasks++; |
| 949 mManager.getLargeIconForUrl(url, mMinIconSize, iconCallback); |
| 950 |
| 951 return view; |
| 952 } |
| 953 |
| 954 @Override |
| 955 public void onLoadingComplete() { |
| 956 RecordHistogram.recordCount100Histogram("NewTabPage.IconsGray", mNum
GrayIcons); |
| 957 RecordHistogram.recordCount100Histogram("NewTabPage.IconsColor", mNu
mColorIcons); |
| 958 RecordHistogram.recordCount100Histogram("NewTabPage.IconsReal", mNum
RealIcons); |
| 959 } |
| 960 } |
| 961 } |
OLD | NEW |