| Index: chrome/android/java_staging/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
|
| diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/ntp/NewTabPageView.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..97c663e07986719dd71cb0b0c4e8ee5553b75a0e
|
| --- /dev/null
|
| +++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
|
| @@ -0,0 +1,961 @@
|
| +// 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.ntp;
|
| +
|
| +import android.annotation.SuppressLint;
|
| +import android.content.Context;
|
| +import android.content.res.Resources;
|
| +import android.graphics.Bitmap;
|
| +import android.graphics.Canvas;
|
| +import android.graphics.Rect;
|
| +import android.graphics.drawable.BitmapDrawable;
|
| +import android.net.Uri;
|
| +import android.os.Build;
|
| +import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
|
| +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
|
| +import android.text.Editable;
|
| +import android.text.TextUtils;
|
| +import android.text.TextWatcher;
|
| +import android.text.method.LinkMovementMethod;
|
| +import android.text.style.ClickableSpan;
|
| +import android.util.AttributeSet;
|
| +import android.view.LayoutInflater;
|
| +import android.view.MotionEvent;
|
| +import android.view.View;
|
| +import android.view.View.OnLayoutChangeListener;
|
| +import android.view.ViewGroup;
|
| +import android.view.ViewStub;
|
| +import android.widget.Button;
|
| +import android.widget.FrameLayout;
|
| +import android.widget.ImageView;
|
| +import android.widget.TextView;
|
| +
|
| +import com.google.android.apps.chrome.R;
|
| +
|
| +import org.chromium.base.CommandLine;
|
| +import org.chromium.base.FieldTrialList;
|
| +import org.chromium.base.VisibleForTesting;
|
| +import org.chromium.base.metrics.RecordHistogram;
|
| +import org.chromium.chrome.browser.ApplicationSwitches;
|
| +import org.chromium.chrome.browser.LogoBridge.Logo;
|
| +import org.chromium.chrome.browser.LogoBridge.LogoObserver;
|
| +import org.chromium.chrome.browser.favicon.FaviconHelper.FaviconImageCallback;
|
| +import org.chromium.chrome.browser.favicon.LargeIconBridge.LargeIconCallback;
|
| +import org.chromium.chrome.browser.ntp.MostVisitedItem.MostVisitedItemManager;
|
| +import org.chromium.chrome.browser.ntp.NewTabPage.OnSearchBoxScrollListener;
|
| +import org.chromium.chrome.browser.profiles.MostVisitedSites.MostVisitedURLsObserver;
|
| +import org.chromium.chrome.browser.profiles.MostVisitedSites.ThumbnailCallback;
|
| +import org.chromium.chrome.browser.util.FeatureUtilities;
|
| +import org.chromium.chrome.browser.util.ViewUtils;
|
| +import org.chromium.chrome.browser.widget.RoundedIconGenerator;
|
| +import org.chromium.ui.text.SpanApplier;
|
| +import org.chromium.ui.text.SpanApplier.SpanInfo;
|
| +
|
| +import java.util.Locale;
|
| +
|
| +/**
|
| + * The native new tab page, represented by some basic data such as title and url, and an Android
|
| + * View that displays the page.
|
| + */
|
| +public class NewTabPageView extends FrameLayout
|
| + implements MostVisitedURLsObserver, OnLayoutChangeListener {
|
| +
|
| + static final int MAX_MOST_VISITED_SITES = 12;
|
| + private static final int SHADOW_COLOR = 0x11000000;
|
| + private static final long SNAP_SCROLL_DELAY_MS = 30;
|
| +
|
| + private static final String ICON_NTP_FIELD_TRIAL_NAME = "IconNTP";
|
| + private static final String ICON_NTP_ENABLED_GROUP = "Enabled";
|
| +
|
| + private ViewGroup mContentView;
|
| + private NewTabScrollView mScrollView;
|
| + private LogoView mSearchProviderLogoView;
|
| + private View mSearchBoxView;
|
| + private TextView mSearchBoxTextView;
|
| + private ImageView mVoiceSearchButton;
|
| + private ViewGroup mMostVisitedLayout;
|
| + private View mMostVisitedPlaceholder;
|
| + private View mOptOutView;
|
| + private View mNoSearchLogoSpacer;
|
| +
|
| + private OnSearchBoxScrollListener mSearchBoxScrollListener;
|
| +
|
| + private NewTabPageManager mManager;
|
| + private MostVisitedDesign mMostVisitedDesign;
|
| + private MostVisitedItem[] mMostVisitedItems;
|
| + private boolean mFirstShow = true;
|
| + private boolean mSearchProviderHasLogo = true;
|
| + private boolean mHasReceivedMostVisitedSites;
|
| + private boolean mPendingSnapScroll;
|
| +
|
| + /**
|
| + * The number of asynchronous tasks that need to complete before the page is done loading.
|
| + * This starts at one to track when the view is finished attaching to the window.
|
| + */
|
| + private int mPendingLoadTasks = 1;
|
| + private boolean mLoadHasCompleted;
|
| +
|
| + private float mUrlFocusChangePercent;
|
| + private boolean mDisableUrlFocusChangeAnimations;
|
| +
|
| + private boolean mSnapshotMostVisitedChanged;
|
| + private int mSnapshotWidth;
|
| + private int mSnapshotHeight;
|
| + private int mSnapshotScrollY;
|
| +
|
| + /**
|
| + * Manages the view interaction with the rest of the system.
|
| + */
|
| + public interface NewTabPageManager extends MostVisitedItemManager {
|
| + /** @return Whether the location bar is shown in the NTP. */
|
| + boolean isLocationBarShownInNTP();
|
| +
|
| + /** @return Whether the document mode opt out promo should be shown. */
|
| + boolean shouldShowOptOutPromo();
|
| +
|
| + /** Called when the document mode opt out promo is shown. */
|
| + void optOutPromoShown();
|
| +
|
| + /** Called when the user clicks "settings" or "ok, got it" on the opt out promo. */
|
| + void optOutPromoClicked(boolean settingsClicked);
|
| +
|
| + /** Opens the bookmarks page in the current tab. */
|
| + void navigateToBookmarks();
|
| +
|
| + /** Opens the recent tabs page in the current tab. */
|
| + void navigateToRecentTabs();
|
| +
|
| + /**
|
| + * Animates the search box up into the omnibox and bring up the keyboard.
|
| + * @param beginVoiceSearch Whether to begin a voice search.
|
| + * @param pastedText Text to paste in the omnibox after it's been focused. May be null.
|
| + */
|
| + void focusSearchBox(boolean beginVoiceSearch, String pastedText);
|
| +
|
| + /**
|
| + * Gets the list of most visited sites.
|
| + * @param observer The observer to be notified with the list of sites.
|
| + * @param numResults The maximum number of sites to retrieve.
|
| + */
|
| + void setMostVisitedURLsObserver(MostVisitedURLsObserver observer, int numResults);
|
| +
|
| + /**
|
| + * Gets a cached thumbnail of a URL.
|
| + * @param url The URL whose thumbnail is being retrieved.
|
| + * @param thumbnailCallback The callback to be notified when the thumbnail is available.
|
| + */
|
| + void getURLThumbnail(String url, ThumbnailCallback thumbnailCallback);
|
| +
|
| + /**
|
| + * Gets the favicon image for a given URL.
|
| + * @param url The URL of the site whose favicon is being requested.
|
| + * @param size The desired size of the favicon in pixels.
|
| + * @param faviconCallback The callback to be notified when the favicon is available.
|
| + */
|
| + void getLocalFaviconImageForURL(
|
| + String url, int size, FaviconImageCallback faviconCallback);
|
| +
|
| + /**
|
| + * Gets the large icon (e.g. favicon or touch icon) for a given URL.
|
| + * @param url The URL of the site whose icon is being requested.
|
| + * @param size The desired size of the icon in pixels.
|
| + * @param callback The callback to be notified when the icon is available.
|
| + */
|
| + void getLargeIconForUrl(String url, int size, LargeIconCallback callback);
|
| +
|
| + /**
|
| + * Navigates to a URL chosen by the search provider when the user clicks on the logo.
|
| + */
|
| + void openLogoLink();
|
| +
|
| + /**
|
| + * Gets the default search provider's logo and calls logoObserver with the result.
|
| + * @param logoObserver The callback to notify when the logo is available.
|
| + */
|
| + void getSearchProviderLogo(LogoObserver logoObserver);
|
| +
|
| + /**
|
| + * Called when the NTP has completely finished loading (all views will be inflated
|
| + * and any dependent resources will have been loaded).
|
| + */
|
| + void onLoadingComplete();
|
| + }
|
| +
|
| + /**
|
| + * Returns a title suitable for display for a link (e.g. a most visited item). If |title| is
|
| + * non-empty, this simply returns it. Otherwise, returns a shortened form of the URL.
|
| + */
|
| + static String getTitleForDisplay(String title, String url) {
|
| + if (TextUtils.isEmpty(title) && url != null) {
|
| + Uri uri = Uri.parse(url);
|
| + String host = uri.getHost();
|
| + String path = uri.getPath();
|
| + if (host == null) host = "";
|
| + if (TextUtils.isEmpty(path) || path.equals("/")) path = "";
|
| + title = host + path;
|
| + }
|
| + return title;
|
| + }
|
| +
|
| + /**
|
| + * Default constructor required for XML inflation.
|
| + */
|
| + public NewTabPageView(Context context, AttributeSet attrs) {
|
| + super(context, attrs);
|
| + }
|
| +
|
| + private boolean isIconNtpEnabled() {
|
| + // Query the field trial state first, to ensure that UMA reports the correct group.
|
| + String fieldTrialGroup = FieldTrialList.findFullName(ICON_NTP_FIELD_TRIAL_NAME);
|
| + CommandLine commandLine = CommandLine.getInstance();
|
| + if (commandLine.hasSwitch(ApplicationSwitches.DISABLE_ICON_NTP)) return false;
|
| + if (commandLine.hasSwitch(ApplicationSwitches.ENABLE_ICON_NTP)) return true;
|
| + return fieldTrialGroup.equals(ICON_NTP_ENABLED_GROUP);
|
| + }
|
| +
|
| + /**
|
| + * Initializes the NTP. This must be called immediately after inflation, before this object is
|
| + * used in any other way.
|
| + *
|
| + * @param manager NewTabPageManager used to perform various actions when the user interacts
|
| + * with the page.
|
| + * @param isSingleUrlBarMode Whether the NTP is in single URL bar mode.
|
| + * @param searchProviderHasLogo Whether the search provider has a logo.
|
| + */
|
| + public void initialize(NewTabPageManager manager, boolean isSingleUrlBarMode,
|
| + boolean searchProviderHasLogo) {
|
| + mManager = manager;
|
| +
|
| + mScrollView = (NewTabScrollView) findViewById(R.id.ntp_scrollview);
|
| + mScrollView.enableBottomShadow(SHADOW_COLOR);
|
| + mContentView = (ViewGroup) findViewById(R.id.ntp_content);
|
| +
|
| + mMostVisitedDesign = isIconNtpEnabled()
|
| + ? new IconMostVisitedDesign(getContext())
|
| + : new ThumbnailMostVisitedDesign(getContext());
|
| + ViewStub mostVisitedLayoutStub = (ViewStub) findViewById(R.id.most_visited_layout_stub);
|
| + mostVisitedLayoutStub.setLayoutResource(mMostVisitedDesign.getMostVisitedLayoutId());
|
| + mMostVisitedLayout = (ViewGroup) mostVisitedLayoutStub.inflate();
|
| +
|
| + mSearchProviderLogoView = (LogoView) findViewById(R.id.search_provider_logo);
|
| + mSearchBoxView = findViewById(R.id.search_box);
|
| + mNoSearchLogoSpacer = findViewById(R.id.no_search_logo_spacer);
|
| +
|
| + mSearchBoxTextView = (TextView) mSearchBoxView.findViewById(R.id.search_box_text);
|
| + String hintText = getResources().getString(R.string.search_or_type_url);
|
| + if (isSingleUrlBarMode) {
|
| + mSearchBoxTextView.setHint(hintText);
|
| + } else {
|
| + mSearchBoxTextView.setContentDescription(hintText);
|
| + }
|
| + mSearchBoxTextView.setOnClickListener(new View.OnClickListener() {
|
| + @Override
|
| + public void onClick(View v) {
|
| + mManager.focusSearchBox(false, null);
|
| + }
|
| + });
|
| + mSearchBoxTextView.addTextChangedListener(new TextWatcher() {
|
| + @Override
|
| + public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
| + }
|
| +
|
| + @Override
|
| + public void onTextChanged(CharSequence s, int start, int before, int count) {
|
| + }
|
| +
|
| + @Override
|
| + public void afterTextChanged(Editable s) {
|
| + if (s.length() == 0) return;
|
| + mManager.focusSearchBox(false, s.toString());
|
| + mSearchBoxTextView.setText("");
|
| + }
|
| + });
|
| +
|
| + mVoiceSearchButton = (ImageView) findViewById(R.id.voice_search_button);
|
| + mVoiceSearchButton.setOnClickListener(new View.OnClickListener() {
|
| + @Override
|
| + public void onClick(View v) {
|
| + mManager.focusSearchBox(true, null);
|
| + }
|
| + });
|
| +
|
| + NewTabPageToolbar toolbar = (NewTabPageToolbar) findViewById(R.id.ntp_toolbar);
|
| + toolbar.getRecentTabsButton().setOnClickListener(new View.OnClickListener() {
|
| + @Override
|
| + public void onClick(View v) {
|
| + mManager.navigateToRecentTabs();
|
| + }
|
| + });
|
| + toolbar.getBookmarksButton().setOnClickListener(new View.OnClickListener() {
|
| + @Override
|
| + public void onClick(View v) {
|
| + mManager.navigateToBookmarks();
|
| + }
|
| + });
|
| +
|
| + initializeSearchBoxScrollHandling();
|
| + addOnLayoutChangeListener(this);
|
| + setSearchProviderHasLogo(searchProviderHasLogo);
|
| +
|
| + mPendingLoadTasks++;
|
| + mManager.setMostVisitedURLsObserver(this,
|
| + mMostVisitedDesign.getNumberOfTiles(searchProviderHasLogo));
|
| +
|
| + if (mManager.shouldShowOptOutPromo()) showOptOutPromo();
|
| + }
|
| +
|
| + private int getTabsMovedIllustration() {
|
| + switch (Build.MANUFACTURER.toLowerCase(Locale.US)) {
|
| + case "samsung":
|
| + return R.drawable.tabs_moved_samsung;
|
| + case "htc":
|
| + return R.drawable.tabs_moved_htc;
|
| + default:
|
| + return R.drawable.tabs_moved_nexus;
|
| + }
|
| + }
|
| +
|
| + private void showOptOutPromo() {
|
| + ViewStub optOutPromoStub = (ViewStub) findViewById(R.id.opt_out_promo_stub);
|
| + mOptOutView = optOutPromoStub.inflate();
|
| + // Fill in opt-out text with Settings link
|
| + TextView optOutText = (TextView) mOptOutView.findViewById(R.id.opt_out_text);
|
| +
|
| + ClickableSpan settingsLink = new ClickableSpan() {
|
| + @Override
|
| + public void onClick(View view) {
|
| + mManager.optOutPromoClicked(true);
|
| + }
|
| +
|
| + // Change link formatting to use our blue control color and no underline
|
| + @Override
|
| + public void updateDrawState(android.text.TextPaint textPaint) {
|
| + textPaint.setColor(getContext().getResources().getColor(
|
| + R.color.light_active_color));
|
| + textPaint.setUnderlineText(false);
|
| + }
|
| + };
|
| +
|
| + optOutText.setText(SpanApplier.applySpans(
|
| + getContext().getString(R.string.tabs_and_apps_opt_out_text),
|
| + new SpanInfo("<link>", "</link>", settingsLink)));
|
| + optOutText.setMovementMethod(LinkMovementMethod.getInstance());
|
| +
|
| + ImageView illustration = (ImageView) mOptOutView.findViewById(R.id.tabs_moved_illustration);
|
| + illustration.setImageResource(getTabsMovedIllustration());
|
| +
|
| + mOptOutView.setVisibility(View.VISIBLE);
|
| + mMostVisitedLayout.setVisibility(View.GONE);
|
| +
|
| + Button gotItButton = (Button) mOptOutView.findViewById(R.id.got_it_button);
|
| + gotItButton.setOnClickListener(new View.OnClickListener() {
|
| + @Override
|
| + public void onClick(View v) {
|
| + mOptOutView.setVisibility(View.GONE);
|
| + mMostVisitedLayout.setVisibility(View.VISIBLE);
|
| + mManager.optOutPromoClicked(false);
|
| + updateMostVisitedPlaceholderVisibility();
|
| + }
|
| + });
|
| + mManager.optOutPromoShown();
|
| + }
|
| +
|
| + private void updateSearchBoxOnScroll() {
|
| + if (mDisableUrlFocusChangeAnimations) return;
|
| +
|
| + float percentage = 0;
|
| + // During startup the view may not be fully initialized, so we only calculate the current
|
| + // percentage if some basic view properties are sane.
|
| + if (mScrollView.getHeight() != 0 && mSearchBoxView.getTop() != 0) {
|
| + int scrollY = mScrollView.getScrollY();
|
| + percentage = Math.max(
|
| + 0f, Math.min(1f, scrollY / (float) mSearchBoxView.getTop()));
|
| + }
|
| +
|
| + updateVisualsForToolbarTransition(percentage);
|
| +
|
| + if (mSearchBoxScrollListener != null) {
|
| + mSearchBoxScrollListener.onScrollChanged(percentage);
|
| + }
|
| + }
|
| +
|
| + private void initializeSearchBoxScrollHandling() {
|
| + final Runnable mSnapScrollRunnable = new Runnable() {
|
| + @Override
|
| + public void run() {
|
| + if (!mPendingSnapScroll) return;
|
| + int scrollY = mScrollView.getScrollY();
|
| + int dividerTop = mMostVisitedLayout.getTop() - mContentView.getPaddingTop();
|
| + if (scrollY > 0 && scrollY < dividerTop) {
|
| + mScrollView.smoothScrollTo(0, scrollY < (dividerTop / 2) ? 0 : dividerTop);
|
| + }
|
| + mPendingSnapScroll = false;
|
| + }
|
| + };
|
| + mScrollView.setOnScrollListener(new NewTabScrollView.OnScrollListener() {
|
| + @Override
|
| + public void onScrollChanged(int l, int t, int oldl, int oldt) {
|
| + if (mPendingSnapScroll) {
|
| + mScrollView.removeCallbacks(mSnapScrollRunnable);
|
| + mScrollView.postDelayed(mSnapScrollRunnable, SNAP_SCROLL_DELAY_MS);
|
| + }
|
| + updateSearchBoxOnScroll();
|
| + }
|
| + });
|
| + mScrollView.setOnTouchListener(new OnTouchListener() {
|
| + @Override
|
| + @SuppressLint("ClickableViewAccessibility")
|
| + public boolean onTouch(View v, MotionEvent event) {
|
| + if (mScrollView.getHandler() == null) return false;
|
| + mScrollView.removeCallbacks(mSnapScrollRunnable);
|
| +
|
| + if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
|
| + || event.getActionMasked() == MotionEvent.ACTION_UP) {
|
| + mPendingSnapScroll = true;
|
| + mScrollView.postDelayed(mSnapScrollRunnable, SNAP_SCROLL_DELAY_MS);
|
| + } else {
|
| + mPendingSnapScroll = false;
|
| + }
|
| + return false;
|
| + }
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Decrements the count of pending load tasks and notifies the manager when the page load
|
| + * is complete.
|
| + */
|
| + private void loadTaskCompleted() {
|
| + assert mPendingLoadTasks > 0;
|
| + mPendingLoadTasks--;
|
| + if (mPendingLoadTasks == 0) {
|
| + if (mLoadHasCompleted) {
|
| + assert false;
|
| + } else {
|
| + mLoadHasCompleted = true;
|
| + mManager.onLoadingComplete();
|
| + mMostVisitedDesign.onLoadingComplete();
|
| + // Load the logo after everything else is finished, since it's lower priority.
|
| + loadSearchProviderLogo();
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Loads the search provider logo (e.g. Google doodle), if any.
|
| + */
|
| + private void loadSearchProviderLogo() {
|
| + mManager.getSearchProviderLogo(new LogoObserver() {
|
| + @Override
|
| + public void onLogoAvailable(Logo logo, boolean fromCache) {
|
| + if (logo == null && fromCache) return;
|
| + mSearchProviderLogoView.setMananger(mManager);
|
| + mSearchProviderLogoView.updateLogo(logo);
|
| + mSnapshotMostVisitedChanged = true;
|
| + }
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Changes the layout depending on whether the selected search provider (e.g. Google, Bing)
|
| + * has a logo.
|
| + * @param hasLogo Whether the search provider has a logo.
|
| + */
|
| + public void setSearchProviderHasLogo(boolean hasLogo) {
|
| + if (hasLogo == mSearchProviderHasLogo) return;
|
| + mSearchProviderHasLogo = hasLogo;
|
| +
|
| + mMostVisitedDesign.setSearchProviderHasLogo(mMostVisitedLayout, hasLogo);
|
| +
|
| + if (!hasLogo) setUrlFocusChangeAnimationPercentInternal(0);
|
| +
|
| + // Hide or show all the views above the Most Visited items.
|
| + int visibility = hasLogo ? View.VISIBLE : View.GONE;
|
| + int childCount = mContentView.getChildCount();
|
| + for (int i = 0; i < childCount; i++) {
|
| + View child = mContentView.getChildAt(i);
|
| + if (child == mMostVisitedLayout) break;
|
| + // Don't change the visibility of a ViewStub as that will automagically inflate it.
|
| + if (child instanceof ViewStub) continue;
|
| + child.setVisibility(visibility);
|
| + }
|
| +
|
| + updateMostVisitedPlaceholderVisibility();
|
| +
|
| + if (hasLogo) setUrlFocusChangeAnimationPercent(mUrlFocusChangePercent);
|
| + mSnapshotMostVisitedChanged = true;
|
| + }
|
| +
|
| + /**
|
| + * Updates whether the NewTabPage should animate on URL focus changes.
|
| + * @param disable Whether to disable the animations.
|
| + */
|
| + void setUrlFocusAnimationsDisabled(boolean disable) {
|
| + if (disable == mDisableUrlFocusChangeAnimations) return;
|
| + mDisableUrlFocusChangeAnimations = disable;
|
| + if (!disable) setUrlFocusChangeAnimationPercent(mUrlFocusChangePercent);
|
| + }
|
| +
|
| + /**
|
| + * @return Whether URL focus animations are currently disabled.
|
| + */
|
| + boolean urlFocusAnimationsDisabled() {
|
| + return mDisableUrlFocusChangeAnimations;
|
| + }
|
| +
|
| + /**
|
| + * Specifies the percentage the URL is focused during an animation. 1.0 specifies that the URL
|
| + * bar has focus and has completed the focus animation. 0 is when the URL bar is does not have
|
| + * any focus.
|
| + *
|
| + * @param percent The percentage of the URL bar focus animation.
|
| + */
|
| + void setUrlFocusChangeAnimationPercent(float percent) {
|
| + mUrlFocusChangePercent = percent;
|
| + if (!mDisableUrlFocusChangeAnimations && mSearchProviderHasLogo) {
|
| + setUrlFocusChangeAnimationPercentInternal(percent);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * @return The percentage that the URL bar is focused during an animation.
|
| + */
|
| + @VisibleForTesting
|
| + float getUrlFocusChangeAnimationPercent() {
|
| + return mUrlFocusChangePercent;
|
| + }
|
| +
|
| + /**
|
| + * Unconditionally sets the percentage the URL is focused during an animation, without updating
|
| + * mUrlFocusChangePercent.
|
| + * @see #setUrlFocusChangeAnimationPercent
|
| + */
|
| + private void setUrlFocusChangeAnimationPercentInternal(float percent) {
|
| + mContentView.setTranslationY(percent * (-mMostVisitedLayout.getTop()
|
| + + mScrollView.getScrollY() + mContentView.getPaddingTop()));
|
| + updateVisualsForToolbarTransition(percent);
|
| + }
|
| +
|
| + private void updateVisualsForToolbarTransition(float transitionPercentage) {
|
| + // Complete the full alpha transition in the first 40% of the animation.
|
| + float searchUiAlpha =
|
| + transitionPercentage >= 0.4f ? 0f : (0.4f - transitionPercentage) * 2.5f;
|
| + // Ensure there are no rounding issues when the animation percent is 0.
|
| + if (transitionPercentage == 0f) searchUiAlpha = 1f;
|
| +
|
| + mSearchProviderLogoView.setAlpha(searchUiAlpha);
|
| + mSearchBoxView.setAlpha(searchUiAlpha);
|
| + }
|
| +
|
| + /**
|
| + * Get the bounds of the search box in relation to the top level NewTabPage view.
|
| + *
|
| + * @param originalBounds The bounding region of the search box without external transforms
|
| + * applied. The delta between this and the transformed bounds determines
|
| + * the amount of scroll applied to this view.
|
| + * @param transformedBounds The bounding region of the search box including any transforms
|
| + * applied by the parent view hierarchy up to the NewTabPage view.
|
| + * This more accurately reflects the current drawing location of the
|
| + * search box.
|
| + */
|
| + void getSearchBoxBounds(Rect originalBounds, Rect transformedBounds) {
|
| + int searchBoxX = (int) mSearchBoxView.getX();
|
| + int searchBoxY = (int) mSearchBoxView.getY();
|
| + originalBounds.set(
|
| + searchBoxX + mSearchBoxView.getPaddingLeft(),
|
| + searchBoxY + mSearchBoxView.getPaddingTop(),
|
| + searchBoxX + mSearchBoxView.getWidth() - mSearchBoxView.getPaddingRight(),
|
| + searchBoxY + mSearchBoxView.getHeight() - mSearchBoxView.getPaddingBottom());
|
| +
|
| + transformedBounds.set(originalBounds);
|
| + View view = (View) mSearchBoxView.getParent();
|
| + while (view != null) {
|
| + transformedBounds.offset(-view.getScrollX(), -view.getScrollY());
|
| + if (view == this) break;
|
| + transformedBounds.offset((int) view.getX(), (int) view.getY());
|
| + view = (View) view.getParent();
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Sets the listener for search box scroll changes.
|
| + * @param listener The listener to be notified on changes.
|
| + */
|
| + void setSearchBoxScrollListener(OnSearchBoxScrollListener listener) {
|
| + mSearchBoxScrollListener = listener;
|
| + if (mSearchBoxScrollListener != null) updateSearchBoxOnScroll();
|
| + }
|
| +
|
| + @Override
|
| + protected void onAttachedToWindow() {
|
| + super.onAttachedToWindow();
|
| + assert mManager != null;
|
| +
|
| + if (mFirstShow) {
|
| + loadTaskCompleted();
|
| + mFirstShow = false;
|
| + } else {
|
| + // Trigger a scroll update when reattaching the window to signal the toolbar that
|
| + // it needs to reset the NTP state.
|
| + if (mManager.isLocationBarShownInNTP()) updateSearchBoxOnScroll();
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void onDetachedFromWindow() {
|
| + super.onDetachedFromWindow();
|
| + setUrlFocusChangeAnimationPercent(0f);
|
| + }
|
| +
|
| + @Override
|
| + protected void onWindowVisibilityChanged(int visibility) {
|
| + super.onWindowVisibilityChanged(visibility);
|
| +
|
| + if (visibility == VISIBLE) {
|
| + mVoiceSearchButton.setVisibility(
|
| + FeatureUtilities.isRecognitionIntentPresent(getContext(), true)
|
| + ? VISIBLE : GONE);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
| + super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
| +
|
| + // Make the search box and logo the same width as the most visited tiles.
|
| + if (mMostVisitedLayout.getVisibility() != GONE) {
|
| + int mostVisitedWidth = MeasureSpec.makeMeasureSpec(
|
| + mMostVisitedLayout.getMeasuredWidth(), MeasureSpec.EXACTLY);
|
| + int searchBoxHeight = MeasureSpec.makeMeasureSpec(
|
| + mSearchBoxView.getMeasuredHeight(), MeasureSpec.EXACTLY);
|
| + int logoHeight = MeasureSpec.makeMeasureSpec(
|
| + mSearchProviderLogoView.getMeasuredHeight(), MeasureSpec.EXACTLY);
|
| + mSearchBoxView.measure(mostVisitedWidth, searchBoxHeight);
|
| + mSearchProviderLogoView.measure(mostVisitedWidth, logoHeight);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * @see org.chromium.chrome.browser.compositor.layouts.content.
|
| + * InvalidationAwareThumbnailProvider#shouldCaptureThumbnail()
|
| + */
|
| + boolean shouldCaptureThumbnail() {
|
| + if (getWidth() == 0 || getHeight() == 0) return false;
|
| +
|
| + return mSnapshotMostVisitedChanged
|
| + || getWidth() != mSnapshotWidth
|
| + || getHeight() != mSnapshotHeight
|
| + || mScrollView.getScrollY() != mSnapshotScrollY;
|
| + }
|
| +
|
| + /**
|
| + * @see org.chromium.chrome.browser.compositor.layouts.content.
|
| + * InvalidationAwareThumbnailProvider#captureThumbnail(Canvas)
|
| + */
|
| + void captureThumbnail(Canvas canvas) {
|
| + mSearchProviderLogoView.endAnimation();
|
| + ViewUtils.captureBitmap(this, canvas);
|
| + mSnapshotWidth = getWidth();
|
| + mSnapshotHeight = getHeight();
|
| + mSnapshotScrollY = mScrollView.getScrollY();
|
| + mSnapshotMostVisitedChanged = false;
|
| + }
|
| +
|
| + // OnLayoutChangeListener overrides
|
| +
|
| + @Override
|
| + public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
| + int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
| + int oldWidth = oldRight - oldLeft;
|
| + int newWidth = right - left;
|
| + if (oldWidth == newWidth) return;
|
| +
|
| + // Re-apply the url focus change amount after a rotation to ensure the views are correctly
|
| + // placed with their new layout configurations.
|
| + setUrlFocusChangeAnimationPercent(mUrlFocusChangePercent);
|
| + }
|
| +
|
| + // MostVisitedURLsObserver implementation
|
| +
|
| + @Override
|
| + public void onMostVisitedURLsAvailable(String[] titles, String[] urls) {
|
| + mMostVisitedLayout.removeAllViews();
|
| +
|
| + MostVisitedItem[] oldItems = mMostVisitedItems;
|
| + int oldItemCount = oldItems == null ? 0 : oldItems.length;
|
| + mMostVisitedItems = new MostVisitedItem[titles.length];
|
| +
|
| + final boolean isInitialLoad = !mHasReceivedMostVisitedSites;
|
| + LayoutInflater inflater = LayoutInflater.from(getContext());
|
| +
|
| + // Add the most visited items to the page.
|
| + for (int i = 0; i < titles.length; i++) {
|
| + final String url = urls[i];
|
| + final String title = titles[i];
|
| +
|
| + // Look for an existing item to reuse.
|
| + MostVisitedItem item = null;
|
| + for (int j = 0; j < oldItemCount; j++) {
|
| + MostVisitedItem oldItem = oldItems[j];
|
| + if (oldItem != null && TextUtils.equals(url, oldItem.getUrl())
|
| + && TextUtils.equals(title, oldItem.getTitle())) {
|
| + item = oldItem;
|
| + item.setIndex(i);
|
| + oldItems[j] = null;
|
| + break;
|
| + }
|
| + }
|
| +
|
| + // If nothing can be reused, create a new item.
|
| + if (item == null) {
|
| + String displayTitle = getTitleForDisplay(title, url);
|
| + View view = mMostVisitedDesign.createMostVisitedItemView(inflater, url, title,
|
| + displayTitle, i, isInitialLoad);
|
| + item = new MostVisitedItem(mManager, title, url, i, view);
|
| + }
|
| +
|
| + mMostVisitedItems[i] = item;
|
| + mMostVisitedLayout.addView(item.getView());
|
| + }
|
| +
|
| + mHasReceivedMostVisitedSites = true;
|
| + updateMostVisitedPlaceholderVisibility();
|
| +
|
| + if (isInitialLoad) {
|
| + loadTaskCompleted();
|
| + // The page contents are initially hidden; otherwise they'll be drawn centered on the
|
| + // page before the most visited sites are available and then jump upwards to make space
|
| + // once the most visited sites are available.
|
| + mContentView.setVisibility(View.VISIBLE);
|
| + }
|
| + mSnapshotMostVisitedChanged = true;
|
| + }
|
| +
|
| + /**
|
| + * Shows the most visited placeholder ("Nothing to see here") if there are no most visited
|
| + * items and there is no search provider logo.
|
| + */
|
| + private void updateMostVisitedPlaceholderVisibility() {
|
| + boolean showPlaceholder = mHasReceivedMostVisitedSites
|
| + && !mManager.shouldShowOptOutPromo()
|
| + && mMostVisitedLayout.getChildCount() == 0
|
| + && !mSearchProviderHasLogo;
|
| +
|
| + mNoSearchLogoSpacer.setVisibility(
|
| + (mSearchProviderHasLogo || showPlaceholder) ? View.GONE : View.INVISIBLE);
|
| +
|
| + if (showPlaceholder) {
|
| + if (mMostVisitedPlaceholder == null) {
|
| + ViewStub mostVisitedPlaceholderStub = (ViewStub) findViewById(
|
| + R.id.most_visited_placeholder_stub);
|
| + mMostVisitedPlaceholder = mostVisitedPlaceholderStub.inflate();
|
| + }
|
| + mMostVisitedLayout.setVisibility(GONE);
|
| + mMostVisitedPlaceholder.setVisibility(VISIBLE);
|
| + return;
|
| + } else if (mMostVisitedPlaceholder != null) {
|
| + mMostVisitedLayout.setVisibility(VISIBLE);
|
| + mMostVisitedPlaceholder.setVisibility(GONE);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Interface for creating the most visited layout and tiles.
|
| + * TODO(newt): delete this once a single design has been chosen.
|
| + */
|
| + private interface MostVisitedDesign {
|
| + int getNumberOfTiles(boolean searchProviderHasLogo);
|
| + int getMostVisitedLayoutId();
|
| + void setSearchProviderHasLogo(View mostVisitedLayout, boolean hasLogo);
|
| + View createMostVisitedItemView(LayoutInflater inflater, String url, String title,
|
| + String displayTitle, int index, boolean isInitialLoad);
|
| + void onLoadingComplete();
|
| + }
|
| +
|
| + /**
|
| + * The old most visited design, where each tile shows a thumbnail of the page, a small favicon,
|
| + * and the title.
|
| + */
|
| + private class ThumbnailMostVisitedDesign implements MostVisitedDesign {
|
| +
|
| + private static final int NUM_TILES = 6;
|
| + private static final int FAVICON_CORNER_RADIUS_DP = 2;
|
| + private static final int FAVICON_TEXT_SIZE_DP = 10;
|
| + private static final int FAVICON_BACKGROUND_COLOR = 0xff969696;
|
| +
|
| + private int mDesiredFaviconSize;
|
| + private RoundedIconGenerator mFaviconGenerator;
|
| +
|
| + ThumbnailMostVisitedDesign(Context context) {
|
| + Resources res = context.getResources();
|
| + mDesiredFaviconSize = res.getDimensionPixelSize(R.dimen.most_visited_favicon_size);
|
| + int desiredFaviconSizeDp = Math.round(
|
| + mDesiredFaviconSize / res.getDisplayMetrics().density);
|
| + mFaviconGenerator = new RoundedIconGenerator(
|
| + context, desiredFaviconSizeDp, desiredFaviconSizeDp, FAVICON_CORNER_RADIUS_DP,
|
| + FAVICON_BACKGROUND_COLOR, FAVICON_TEXT_SIZE_DP);
|
| + }
|
| +
|
| + @Override
|
| + public int getNumberOfTiles(boolean searchProviderHasLogo) {
|
| + return NUM_TILES;
|
| + }
|
| +
|
| + @Override
|
| + public int getMostVisitedLayoutId() {
|
| + return R.layout.most_visited_layout;
|
| + }
|
| +
|
| + @Override
|
| + public void setSearchProviderHasLogo(View mostVisitedLayout, boolean hasLogo) {}
|
| +
|
| + @Override
|
| + public View createMostVisitedItemView(LayoutInflater inflater, final String url,
|
| + String title, String displayTitle, int index, final boolean isInitialLoad) {
|
| + final MostVisitedItemView view = (MostVisitedItemView) inflater.inflate(
|
| + R.layout.most_visited_item, mMostVisitedLayout, false);
|
| + view.init(displayTitle);
|
| +
|
| + ThumbnailCallback thumbnailCallback = new ThumbnailCallback() {
|
| + @Override
|
| + public void onMostVisitedURLsThumbnailAvailable(Bitmap thumbnail) {
|
| + view.setThumbnail(thumbnail);
|
| + mSnapshotMostVisitedChanged = true;
|
| + if (isInitialLoad) loadTaskCompleted();
|
| + }
|
| + };
|
| + if (isInitialLoad) mPendingLoadTasks++;
|
| + mManager.getURLThumbnail(url, thumbnailCallback);
|
| +
|
| + FaviconImageCallback faviconCallback = new FaviconImageCallback() {
|
| + @Override
|
| + public void onFaviconAvailable(Bitmap image, String iconUrl) {
|
| + if (image == null) {
|
| + image = mFaviconGenerator.generateIconForUrl(url);
|
| + }
|
| + view.setFavicon(image);
|
| + mSnapshotMostVisitedChanged = true;
|
| + if (isInitialLoad) loadTaskCompleted();
|
| + }
|
| + };
|
| + if (isInitialLoad) mPendingLoadTasks++;
|
| + mManager.getLocalFaviconImageForURL(url, mDesiredFaviconSize, faviconCallback);
|
| +
|
| + return view;
|
| + }
|
| +
|
| + @Override
|
| + public void onLoadingComplete() {}
|
| + }
|
| +
|
| + /**
|
| + * The new-fangled design for most visited tiles, where each tile shows a large icon and title.
|
| + */
|
| + private class IconMostVisitedDesign implements MostVisitedDesign {
|
| +
|
| + private static final int NUM_TILES = 8;
|
| + private static final int NUM_TILES_NO_LOGO = 12;
|
| +
|
| + private static final int ICON_CORNER_RADIUS_DP = 4;
|
| + private static final int ICON_TEXT_SIZE_DP = 20;
|
| + private static final int ICON_BACKGROUND_COLOR = 0xff787878;
|
| + private static final int ICON_MIN_SIZE_PX = 48;
|
| +
|
| + private int mMinIconSize;
|
| + private int mDesiredIconSize;
|
| + private RoundedIconGenerator mIconGenerator;
|
| +
|
| + private int mNumGrayIcons;
|
| + private int mNumColorIcons;
|
| + private int mNumRealIcons;
|
| +
|
| + IconMostVisitedDesign(Context context) {
|
| + Resources res = context.getResources();
|
| + mDesiredIconSize = res.getDimensionPixelSize(R.dimen.icon_most_visited_icon_size);
|
| + // On ldpi devices, mDesiredIconSize could be even smaller than ICON_MIN_SIZE_PX.
|
| + mMinIconSize = Math.min(mDesiredIconSize, ICON_MIN_SIZE_PX);
|
| + int desiredIconSizeDp = Math.round(
|
| + mDesiredIconSize / res.getDisplayMetrics().density);
|
| + mIconGenerator = new RoundedIconGenerator(
|
| + context, desiredIconSizeDp, desiredIconSizeDp, ICON_CORNER_RADIUS_DP,
|
| + ICON_BACKGROUND_COLOR, ICON_TEXT_SIZE_DP);
|
| + }
|
| +
|
| + @Override
|
| + public int getNumberOfTiles(boolean searchProviderHasLogo) {
|
| + return searchProviderHasLogo ? NUM_TILES : NUM_TILES_NO_LOGO;
|
| + }
|
| +
|
| + @Override
|
| + public int getMostVisitedLayoutId() {
|
| + return R.layout.icon_most_visited_layout;
|
| + }
|
| +
|
| + @Override
|
| + public void setSearchProviderHasLogo(View mostVisitedLayout, boolean hasLogo) {
|
| + if (hasLogo) {
|
| + int paddingTop = getResources().getDimensionPixelSize(
|
| + R.dimen.icon_most_visited_layout_padding_top);
|
| + int paddingSide = getResources().getDimensionPixelSize(
|
| + R.dimen.icon_most_visited_layout_padding_side);
|
| + mostVisitedLayout.setPadding(paddingSide, paddingTop, paddingSide, 0);
|
| + } else {
|
| + int paddingTop = getResources().getDimensionPixelSize(
|
| + R.dimen.icon_most_visited_layout_no_logo_padding_top);
|
| + mostVisitedLayout.setPadding(0, paddingTop, 0, 0);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public View createMostVisitedItemView(LayoutInflater inflater, final String url,
|
| + String title, String displayTitle, int index, final boolean isInitialLoad) {
|
| + final IconMostVisitedItemView view = (IconMostVisitedItemView) inflater.inflate(
|
| + R.layout.icon_most_visited_item, mMostVisitedLayout, false);
|
| + view.setTitle(displayTitle);
|
| +
|
| + LargeIconCallback iconCallback = new LargeIconCallback() {
|
| + @Override
|
| + public void onLargeIconAvailable(Bitmap icon, int fallbackColor) {
|
| + if (icon == null) {
|
| + mIconGenerator.setBackgroundColor(fallbackColor);
|
| + icon = mIconGenerator.generateIconForUrl(url);
|
| + view.setIcon(new BitmapDrawable(getResources(), icon));
|
| + if (isInitialLoad) {
|
| + if (fallbackColor == ICON_BACKGROUND_COLOR) {
|
| + mNumGrayIcons++;
|
| + } else {
|
| + mNumColorIcons++;
|
| + }
|
| + }
|
| + } else {
|
| + RoundedBitmapDrawable roundedIcon = RoundedBitmapDrawableFactory.create(
|
| + getResources(), icon);
|
| + int cornerRadius = Math.round(ICON_CORNER_RADIUS_DP
|
| + * getResources().getDisplayMetrics().density * icon.getWidth()
|
| + / mDesiredIconSize);
|
| + roundedIcon.setCornerRadius(cornerRadius);
|
| + roundedIcon.setAntiAlias(true);
|
| + roundedIcon.setFilterBitmap(true);
|
| + view.setIcon(roundedIcon);
|
| + if (isInitialLoad) mNumRealIcons++;
|
| + }
|
| + mSnapshotMostVisitedChanged = true;
|
| + if (isInitialLoad) loadTaskCompleted();
|
| + }
|
| + };
|
| + if (isInitialLoad) mPendingLoadTasks++;
|
| + mManager.getLargeIconForUrl(url, mMinIconSize, iconCallback);
|
| +
|
| + return view;
|
| + }
|
| +
|
| + @Override
|
| + public void onLoadingComplete() {
|
| + RecordHistogram.recordCount100Histogram("NewTabPage.IconsGray", mNumGrayIcons);
|
| + RecordHistogram.recordCount100Histogram("NewTabPage.IconsColor", mNumColorIcons);
|
| + RecordHistogram.recordCount100Histogram("NewTabPage.IconsReal", mNumRealIcons);
|
| + }
|
| + }
|
| +}
|
|
|