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