blob: 1e03115ffa0f13651c22856a526a847fce8928a9 [file] [log] [blame]
// 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.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.view.MarginLayoutParamsCompat;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.compositor.layouts.content.InvalidationAwareThumbnailProvider;
import org.chromium.chrome.browser.explore_sites.ExperimentalExploreSitesSection;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.ntp.NewTabPage.OnSearchBoxScrollListener;
import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager;
import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.suggestions.SiteSection;
import org.chromium.chrome.browser.suggestions.SiteSectionViewHolder;
import org.chromium.chrome.browser.suggestions.SuggestionsConfig;
import org.chromium.chrome.browser.suggestions.SuggestionsDependencyFactory;
import org.chromium.chrome.browser.suggestions.Tile;
import org.chromium.chrome.browser.suggestions.TileGridLayout;
import org.chromium.chrome.browser.suggestions.TileGroup;
import org.chromium.chrome.browser.suggestions.TileRenderer;
import org.chromium.chrome.browser.suggestions.TileView;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.util.MathUtils;
import org.chromium.chrome.browser.util.ViewUtils;
import org.chromium.chrome.browser.vr.VrModeObserver;
import org.chromium.chrome.browser.vr.VrModuleProvider;
import org.chromium.chrome.browser.widget.displaystyle.UiConfig;
import org.chromium.ui.base.DeviceFormFactor;
/**
* Layout for the new tab page. This positions the page elements in the correct vertical positions.
* There are no separate phone and tablet UIs; this layout adapts based on the available space.
*/
public class NewTabPageLayout extends LinearLayout implements TileGroup.Observer, VrModeObserver {
private static final String TAG = "NewTabPageLayout";
/**
* Parameter for the simplified NTP ablation experiment arm which removes the additional
* suggestions sections without replacing them with shortcut buttons.
*/
private static final String PARAM_SIMPLIFIED_NTP_ABLATION = "simplified_ntp_ablation";
private final int mTileGridLayoutBleed;
private final int mSearchboxShadowWidth;
private View mMiddleSpacer; // Spacer between toolbar and Most Likely.
private LogoView mSearchProviderLogoView;
private View mSearchBoxView;
private ViewGroup mSiteSectionView;
private SiteSectionViewHolder mSiteSectionViewHolder;
private ImageView mVoiceSearchButton;
private View mTileGridPlaceholder;
private View mNoSearchLogoSpacer;
private ViewGroup mShortcutsView;
@Nullable
private View mExploreSectionView; // View is null if explore flag is disabled.
@Nullable
private ExperimentalExploreSitesSection mExploreSection; // Null when explore sites disabled.
private OnSearchBoxScrollListener mSearchBoxScrollListener;
private NewTabPageManager mManager;
private Tab mTab;
private LogoDelegateImpl mLogoDelegate;
private TileGroup mTileGroup;
private UiConfig mUiConfig;
/**
* Whether the tiles shown in the layout have finished loading.
* With {@link #mHasShownView}, it's one of the 2 flags used to track initialisation progress.
*/
private boolean mTilesLoaded;
/**
* Whether the view has been shown at least once.
* With {@link #mTilesLoaded}, it's one of the 2 flags used to track initialization progress.
*/
private boolean mHasShownView;
private boolean mSearchProviderHasLogo = true;
private boolean mSearchProviderIsGoogle;
private boolean mInitialized;
private float mUrlFocusChangePercent;
private boolean mDisableUrlFocusChangeAnimations;
private boolean mIsViewMoving;
/** Flag used to request some layout changes after the next layout pass is completed. */
private boolean mTileCountChanged;
private boolean mSnapshotTileGridChanged;
/**
* Vertical inset to add to the top and bottom of the search box bounds. May be 0 if no inset
* should be applied. See {@link Rect#inset(int, int)}.
*/
private int mSearchBoxBoundsVerticalInset;
private ScrollDelegate mScrollDelegate;
/**
* @return Whether the simplified NTP ablation experiment arm which removes the additional
* suggestions sections without replacing them with shortcut buttons is enabled.
*/
public static boolean isSimplifiedNtpAblationEnabled() {
return ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
ChromeFeatureList.SIMPLIFIED_NTP, PARAM_SIMPLIFIED_NTP_ABLATION, false);
}
/**
* A delegate used to obtain information about scroll state and perform various scroll
* functions.
*/
public interface ScrollDelegate {
/**
* @return Whether the scroll view is initialized. If false, the other delegate methods
* may not be valid.
*/
boolean isScrollViewInitialized();
/**
* Checks whether the child at a given position is visible.
* @param position The position of the child to check.
* @return True if the child is at least partially visible.
*/
boolean isChildVisibleAtPosition(int position);
/**
* @return The vertical scroll offset of the view containing this layout in pixels. Not
* valid until scroll view is initialized.
*/
int getVerticalScrollOffset();
/**
* Snaps the scroll point of the scroll view to prevent the user from scrolling to midway
* through a transition.
*/
void snapScroll();
}
/**
* Constructor for inflating from XML.
*/
public NewTabPageLayout(Context context, AttributeSet attrs) {
super(context, attrs);
Resources res = getResources();
mTileGridLayoutBleed = res.getDimensionPixelSize(R.dimen.tile_grid_layout_bleed);
mSearchboxShadowWidth = res.getDimensionPixelOffset(R.dimen.ntp_search_box_shadow_width);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMiddleSpacer = findViewById(R.id.ntp_middle_spacer);
mSearchProviderLogoView = findViewById(R.id.search_provider_logo);
mSearchBoxView = findViewById(R.id.search_box);
insertSiteSectionView();
if (ChromeFeatureList.isEnabled(ChromeFeatureList.EXPLORE_SITES)) {
ViewStub exploreStub = findViewById(R.id.explore_sites_stub);
mExploreSectionView = exploreStub.inflate();
}
}
/**
* Initializes the NewTabPageLayout. 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 tab The Tab that is showing this new tab page.
* @param searchProviderHasLogo Whether the search provider has a logo.
* @param searchProviderIsGoogle Whether the search provider is Google.
* @param scrollDelegate The delegate used to obtain information about scroll state.
* @param contextMenuManager The manager for long-press context menus.
* @param uiConfig UiConfig that provides display information about this view.
*/
public void initialize(NewTabPageManager manager, Tab tab, TileGroup.Delegate tileGroupDelegate,
boolean searchProviderHasLogo, boolean searchProviderIsGoogle,
ScrollDelegate scrollDelegate, ContextMenuManager contextMenuManager,
UiConfig uiConfig) {
TraceEvent.begin(TAG + ".initialize()");
mScrollDelegate = scrollDelegate;
mTab = tab;
mManager = manager;
mUiConfig = uiConfig;
Profile profile = Profile.getLastUsedProfile();
OfflinePageBridge offlinePageBridge =
SuggestionsDependencyFactory.getInstance().getOfflinePageBridge(profile);
TileRenderer tileRenderer =
new TileRenderer(mTab.getActivity(), SuggestionsConfig.getTileStyle(mUiConfig),
getTileTitleLines(), mManager.getImageFetcher());
mTileGroup = new TileGroup(tileRenderer, mManager, contextMenuManager, tileGroupDelegate,
/* observer = */ this, offlinePageBridge);
mSiteSectionViewHolder = SiteSection.createViewHolder(getSiteSectionView(), mUiConfig);
mSiteSectionViewHolder.bindDataSource(mTileGroup, tileRenderer);
if (ChromeFeatureList.isEnabled(ChromeFeatureList.EXPLORE_SITES)) {
mExploreSection = new ExperimentalExploreSitesSection(
mExploreSectionView, profile, mManager.getNavigationDelegate());
}
mSearchProviderLogoView = findViewById(R.id.search_provider_logo);
mLogoDelegate = new LogoDelegateImpl(
mManager.getNavigationDelegate(), mSearchProviderLogoView, profile);
mSearchBoxView = findViewById(R.id.search_box);
if (SuggestionsConfig.useModernLayout()) {
mSearchBoxView.setBackgroundResource(R.drawable.ntp_search_box);
mSearchBoxView.getLayoutParams().height =
getResources().getDimensionPixelSize(R.dimen.ntp_search_box_height_modern);
if (!DeviceFormFactor.isWindowOnTablet(mTab.getWindowAndroid())) {
mSearchBoxBoundsVerticalInset = getResources().getDimensionPixelSize(
R.dimen.ntp_search_box_bounds_vertical_inset_modern);
}
}
mNoSearchLogoSpacer = findViewById(R.id.no_search_logo_spacer);
initializeShortcuts();
initializeSearchBoxTextView();
initializeVoiceSearchButton();
initializeLayoutChangeListener();
setSearchProviderInfo(searchProviderHasLogo, searchProviderIsGoogle);
mSearchProviderLogoView.showSearchProviderInitialView();
mTileGroup.startObserving(getMaxTileRows() * getMaxTileColumns());
VrModuleProvider.registerVrModeObserver(this);
if (VrModuleProvider.getDelegate().isInVr()) onEnterVr();
manager.addDestructionObserver(NewTabPageLayout.this ::onDestroy);
mInitialized = true;
TraceEvent.end(TAG + ".initialize()");
}
/**
* @return The {@link ScrollDelegate} for this class.
*/
ScrollDelegate getScrollDelegate() {
return mScrollDelegate;
}
/**
* Sets up the hint text and event handlers for the search box text view.
*/
private void initializeSearchBoxTextView() {
TraceEvent.begin(TAG + ".initializeSearchBoxTextView()");
final TextView searchBoxTextView = mSearchBoxView.findViewById(R.id.search_box_text);
String hintText = getResources().getString(R.string.search_or_type_web_address);
if (!DeviceFormFactor.isNonMultiDisplayContextOnTablet(getContext())
|| SuggestionsConfig.useModernLayout()) {
searchBoxTextView.setHint(hintText);
} else {
searchBoxTextView.setContentDescription(hintText);
}
searchBoxTextView.setOnClickListener(v -> mManager.focusSearchBox(false, null));
searchBoxTextView.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());
searchBoxTextView.setText("");
}
});
TraceEvent.end(TAG + ".initializeSearchBoxTextView()");
}
/**
* Updates the small search engine logo shown in the search box.
*/
private void updateSearchBoxLogo() {
TextView searchBoxTextView = mSearchBoxView.findViewById(R.id.search_box_text);
LocaleManager localeManager = LocaleManager.getInstance();
if (mSearchProviderIsGoogle && !localeManager.hasCompletedSearchEnginePromo()
&& !localeManager.hasShownSearchEnginePromoThisSession()
&& ChromeFeatureList.isEnabled(ChromeFeatureList.NTP_SHOW_GOOGLE_G_IN_OMNIBOX)) {
searchBoxTextView.setCompoundDrawablePadding(
getResources().getDimensionPixelOffset(R.dimen.ntp_search_box_logo_padding));
ApiCompatibilityUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(
searchBoxTextView, R.drawable.ic_logo_googleg_24dp, 0, 0, 0);
} else {
searchBoxTextView.setCompoundDrawablePadding(0);
// Not using the relative version of this call because we only want to clear
// the drawables.
searchBoxTextView.setCompoundDrawables(null, null, null, null);
}
}
private void initializeVoiceSearchButton() {
TraceEvent.begin(TAG + ".initializeVoiceSearchButton()");
mVoiceSearchButton = findViewById(R.id.voice_search_button);
mVoiceSearchButton.setOnClickListener(v -> mManager.focusSearchBox(true, null));
if (SuggestionsConfig.useModernLayout()
&& !DeviceFormFactor.isWindowOnTablet(mTab.getWindowAndroid())) {
MarginLayoutParamsCompat.setMarginEnd(
(MarginLayoutParams) mVoiceSearchButton.getLayoutParams(),
getResources().getDimensionPixelSize(
R.dimen.ntp_search_box_voice_search_margin_end_modern));
}
TraceEvent.end(TAG + ".initializeVoiceSearchButton()");
}
private void initializeLayoutChangeListener() {
TraceEvent.begin(TAG + ".initializeLayoutChangeListener()");
addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
int oldHeight = oldBottom - oldTop;
int newHeight = bottom - top;
if (oldHeight == newHeight && !mTileCountChanged) return;
mTileCountChanged = false;
// Re-apply the url focus change amount after a rotation to ensure the views are
// correctly placed with their new layout configurations.
onUrlFocusAnimationChanged();
updateSearchBoxOnScroll();
// The positioning of elements may have been changed (since the elements expand
// to fill the available vertical space), so adjust the scroll.
mScrollDelegate.snapScroll();
});
TraceEvent.end(TAG + ".initializeLayoutChangeListener()");
}
/**
* Updates the search box when the parent view's scroll position is changed.
*/
void updateSearchBoxOnScroll() {
if (mDisableUrlFocusChangeAnimations || mIsViewMoving) return;
// When the page changes (tab switching or new page loading), it is possible that events
// (e.g. delayed view change notifications) trigger calls to these methods after
// the current page changes. We check it again to make sure we don't attempt to update the
// wrong page.
if (!mManager.isCurrentPage()) return;
if (mSearchBoxScrollListener != null) {
mSearchBoxScrollListener.onNtpScrollChanged(getToolbarTransitionPercentage());
}
}
/**
* Calculates the percentage (between 0 and 1) of the transition from the search box to the
* omnibox at the top of the New Tab Page, which is determined by the amount of scrolling and
* the position of the search box.
*
* @return the transition percentage
*/
private float getToolbarTransitionPercentage() {
// During startup the view may not be fully initialized.
if (!mScrollDelegate.isScrollViewInitialized()) return 0f;
if (!mScrollDelegate.isChildVisibleAtPosition(0)) {
// getVerticalScrollOffset is valid only for the scroll view if the first item is
// visible. If the first item is not visible, we must have scrolled quite far and we
// know the toolbar transition should be 100%. This might be the initial scroll position
// due to the scroll restore feature, so the search box will not have been laid out yet.
return 1f;
}
// During startup the view may not be fully initialized, so we only calculate the current
// percentage if some basic view properties (position of the search box) are sane.
int searchBoxTop = mSearchBoxView.getTop();
if (searchBoxTop == 0) return 0f;
// For all other calculations, add the search box padding, because it defines where the
// visible "border" of the search box is.
searchBoxTop += mSearchBoxView.getPaddingTop();
final int scrollY = mScrollDelegate.getVerticalScrollOffset();
// Use int pixel size instead of float dimension to avoid precision error on the percentage.
final float transitionLength =
getResources().getDimensionPixelSize(R.dimen.ntp_search_box_transition_length);
// Tab strip height is zero on phones, nonzero on tablets.
int tabStripHeight = getResources().getDimensionPixelSize(R.dimen.tab_strip_height);
// |scrollY - searchBoxTop + tabStripHeight| gives the distance the search bar is from the
// top of the tab.
return MathUtils.clamp(
(scrollY - searchBoxTop + tabStripHeight + transitionLength) / transitionLength, 0f,
1f);
}
private void insertSiteSectionView() {
mSiteSectionView = SiteSection.inflateSiteSection(this);
ViewGroup.LayoutParams layoutParams = mSiteSectionView.getLayoutParams();
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
mSiteSectionView.setLayoutParams(layoutParams);
int insertionPoint = indexOfChild(mMiddleSpacer) + 1;
addView(mSiteSectionView, insertionPoint);
}
/**
* @return the embedded {@link TileGridLayout}.
*/
public ViewGroup getSiteSectionView() {
return mSiteSectionView;
}
/**
* @return The fake search box view.
*/
public View getSearchBoxView() {
return mSearchBoxView;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
unifyElementWidths();
}
/**
* @return The placeholder that is shown above the fold when there is no other content to show,
* or null if it has not been inflated yet.
*/
@VisibleForTesting
@Nullable
public View getPlaceholder() {
return mTileGridPlaceholder;
}
@VisibleForTesting
public TileGroup getTileGroup() {
return mTileGroup;
}
/**
* Should be called every time one of the flags used to track initialization progress changes.
* Finalizes initialization once all the preliminary steps are complete.
*
* @see #mHasShownView
* @see #mTilesLoaded
*/
private void onInitializationProgressChanged() {
if (!hasLoadCompleted()) return;
mManager.onLoadingComplete();
// Load the logo after everything else is finished, since it's lower priority.
loadSearchProviderLogo();
}
/**
* To be called to notify that the tiles have finished loading. Will do nothing if a load was
* previously completed.
*/
public void onTilesLoaded() {
if (mTilesLoaded) return;
mTilesLoaded = true;
onInitializationProgressChanged();
}
/**
* Loads the search provider logo (e.g. Google doodle), if any.
*/
public void loadSearchProviderLogo() {
if (!mSearchProviderHasLogo) return;
mSearchProviderLogoView.showSearchProviderInitialView();
mLogoDelegate.getSearchProviderLogo((logo, fromCache) -> {
if (logo == null && fromCache) return;
mSearchProviderLogoView.setDelegate(mLogoDelegate);
mSearchProviderLogoView.updateLogo(logo);
mSnapshotTileGridChanged = 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.
* @param isGoogle Whether the search provider is Google.
*/
public void setSearchProviderInfo(boolean hasLogo, boolean isGoogle) {
if (hasLogo == mSearchProviderHasLogo && isGoogle == mSearchProviderIsGoogle
&& mInitialized) {
return;
}
mSearchProviderHasLogo = hasLogo;
mSearchProviderIsGoogle = isGoogle;
updateTileGridPadding();
// Hide or show the views above the tile grid as needed, including logo, search box, and
// spacers.
int visibility = mSearchProviderHasLogo ? View.VISIBLE : View.GONE;
int logoVisibility = shouldShowLogo() ? View.VISIBLE : View.GONE;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mShortcutsView != null && child == mShortcutsView) break;
if (child == mSiteSectionViewHolder.itemView) break;
// Don't change the visibility of a ViewStub as that will automagically inflate it.
if (child instanceof ViewStub) continue;
if (child == mSearchProviderLogoView) {
child.setVisibility(logoVisibility);
} else {
child.setVisibility(visibility);
}
}
updateTileGridPlaceholderVisibility();
onUrlFocusAnimationChanged();
updateSearchBoxLogo();
mSnapshotTileGridChanged = true;
}
/**
* Updates the padding for the tile grid based on what is shown above it.
*/
private void updateTileGridPadding() {
int paddingTop;
if (mShortcutsView != null) {
// If the shortcuts view is visible, padding will be built into that view.
paddingTop = 0;
} else {
int paddingWithLogoId = SuggestionsConfig.useModernLayout()
? R.dimen.tile_grid_layout_modern_padding_top
: R.dimen.tile_grid_layout_padding_top;
// Set a bit more top padding on the tile grid if there is no logo.
paddingTop = getResources().getDimensionPixelSize(shouldShowLogo()
? paddingWithLogoId
: R.dimen.tile_grid_layout_no_logo_padding_top);
}
mSiteSectionViewHolder.itemView.setPadding(
0, paddingTop, 0, mSiteSectionViewHolder.itemView.getPaddingBottom());
}
/**
* 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) onUrlFocusAnimationChanged();
}
/**
* @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;
onUrlFocusAnimationChanged();
}
/**
* @return The percentage that the URL bar is focused during an animation.
*/
@VisibleForTesting
float getUrlFocusChangeAnimationPercent() {
return mUrlFocusChangePercent;
}
void onUrlFocusAnimationChanged() {
if (mDisableUrlFocusChangeAnimations || mIsViewMoving) return;
// Translate so that the search box is at the top, but only upwards.
float percent = mSearchProviderHasLogo ? mUrlFocusChangePercent : 0;
int basePosition = mScrollDelegate.getVerticalScrollOffset() + getPaddingTop();
int target = Math.max(basePosition,
mSearchBoxView.getBottom() - mSearchBoxView.getPaddingBottom()
- mSearchBoxBoundsVerticalInset);
setTranslationY(percent * (basePosition - target));
}
/**
* Sets whether this view is currently moving within its parent view. When the view is moving
* certain animations will be disabled or prevented.
* @param isViewMoving Whether this view is currently moving.
*/
void setIsViewMoving(boolean isViewMoving) {
mIsViewMoving = isViewMoving;
}
/**
* Updates the opacity of the search box when scrolling.
*
* @param alpha opacity (alpha) value to use.
*/
public void setSearchBoxAlpha(float alpha) {
mSearchBoxView.setAlpha(alpha);
// Disable the search box contents if it is the process of being animated away.
ViewUtils.setEnabledRecursive(mSearchBoxView, mSearchBoxView.getAlpha() == 1.0f);
}
/**
* Updates the opacity of the search provider logo when scrolling.
*
* @param alpha opacity (alpha) value to use.
*/
public void setSearchProviderLogoAlpha(float alpha) {
mSearchProviderLogoView.setAlpha(alpha);
}
/**
* Set the search box background drawable.
*
* @param drawable The search box background.
*/
public void setSearchBoxBackground(Drawable drawable) {
mSearchBoxView.setBackground(drawable);
}
/**
* Get the bounds of the search box in relation to the top level {@code parentView}.
*
* @param bounds The current drawing location of the search box.
* @param translation The translation applied to the search box by the parent view hierarchy up
* to the {@code parentView}.
* @param parentView The top level parent view used to translate search box bounds.
*/
void getSearchBoxBounds(Rect bounds, Point translation, View parentView) {
int searchBoxX = (int) mSearchBoxView.getX();
int searchBoxY = (int) mSearchBoxView.getY();
bounds.set(searchBoxX + mSearchBoxView.getPaddingLeft(),
searchBoxY + mSearchBoxView.getPaddingTop(),
searchBoxX + mSearchBoxView.getWidth() - mSearchBoxView.getPaddingRight(),
searchBoxY + mSearchBoxView.getHeight() - mSearchBoxView.getPaddingBottom());
translation.set(0, 0);
View view = mSearchBoxView;
while (true) {
view = (View) view.getParent();
if (view == null) {
// The |mSearchBoxView| is not a child of this view. This can happen if the
// RecyclerView detaches the NewTabPageLayout after it has been scrolled out of
// view. Set the translation to the minimum Y value as an approximation.
translation.y = Integer.MIN_VALUE;
break;
}
translation.offset(-view.getScrollX(), -view.getScrollY());
if (view == parentView) break;
translation.offset((int) view.getX(), (int) view.getY());
}
bounds.offset(translation.x, translation.y);
if (translation.y != Integer.MIN_VALUE) {
bounds.inset(0, mSearchBoxBoundsVerticalInset);
}
}
/**
* 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 (!mHasShownView) {
mHasShownView = true;
onInitializationProgressChanged();
NewTabPageUma.recordSearchAvailableLoadTime(mTab.getActivity());
TraceEvent.instant("NewTabPageSearchAvailable)");
}
}
/**
* Update the visibility of the voice search button based on whether the feature is currently
* enabled.
*/
void updateVoiceSearchButtonVisibility() {
mVoiceSearchButton.setVisibility(mManager.isVoiceSearchEnabled() ? VISIBLE : GONE);
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
// On first run, the NewTabPageLayout is initialized behind the First Run Experience,
// meaning the UiConfig will pickup the screen layout then. However onConfigurationChanged
// is not called on orientation changes until the FRE is completed. This means that if a
// user starts the FRE in one orientation, changes an orientation and then leaves the FRE
// the UiConfig will have the wrong orientation. https://crbug.com/683886.
mUiConfig.updateDisplayStyle();
if (visibility == VISIBLE) {
updateVoiceSearchButtonVisibility();
}
}
/**
* @see InvalidationAwareThumbnailProvider#shouldCaptureThumbnail()
*/
public boolean shouldCaptureThumbnail() {
return mSnapshotTileGridChanged;
}
/**
* Should be called before a thumbnail of the parent view is captured.
* @see InvalidationAwareThumbnailProvider#captureThumbnail(Canvas)
*/
public void onPreCaptureThumbnail() {
mSearchProviderLogoView.endFadeAnimation();
mSnapshotTileGridChanged = false;
}
/**
* 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 updateTileGridPlaceholderVisibility() {
boolean showPlaceholder =
mTileGroup.hasReceivedData() && mTileGroup.isEmpty() && !mSearchProviderHasLogo;
mNoSearchLogoSpacer.setVisibility(
(mSearchProviderHasLogo || showPlaceholder) ? View.GONE : View.INVISIBLE);
mSiteSectionViewHolder.itemView.setVisibility(showPlaceholder ? GONE : VISIBLE);
if (showPlaceholder) {
if (mTileGridPlaceholder == null) {
ViewStub placeholderStub = findViewById(R.id.tile_grid_placeholder_stub);
mTileGridPlaceholder = placeholderStub.inflate();
}
mTileGridPlaceholder.setVisibility(VISIBLE);
} else if (mTileGridPlaceholder != null) {
mTileGridPlaceholder.setVisibility(GONE);
}
}
private static int getMaxTileRows() {
return 2;
}
/**
* Determines The maximum number of tiles to try and fit in a row. On smaller screens, there
* may not be enough space to fit all of them.
*/
private int getMaxTileColumns() {
if (!mUiConfig.getCurrentDisplayStyle().isSmall()
&& SuggestionsConfig.getTileStyle(mUiConfig) == TileView.Style.CLASSIC_CONDENSED) {
return 5;
}
return 4;
}
private static int getTileTitleLines() {
return 1;
}
private boolean shouldShowLogo() {
return mSearchProviderHasLogo;
}
private boolean hasLoadCompleted() {
return mHasShownView && mTilesLoaded;
}
// TileGroup.Observer interface.
@Override
public void onTileDataChanged() {
mSiteSectionViewHolder.refreshData();
mSnapshotTileGridChanged = true;
// The page contents are initially hidden; otherwise they'll be drawn centered on the page
// before the tiles are available and then jump upwards to make space once the tiles are
// available.
if (getVisibility() != View.VISIBLE) setVisibility(View.VISIBLE);
}
@Override
public void onTileCountChanged() {
// If the number of tile rows change while the URL bar is focused, the icons'
// position will be wrong. Schedule the translation to be updated.
if (mUrlFocusChangePercent == 1f) mTileCountChanged = true;
updateTileGridPlaceholderVisibility();
}
@Override
public void onTileIconChanged(Tile tile) {
mSiteSectionViewHolder.updateIconView(tile);
mSnapshotTileGridChanged = true;
}
@Override
public void onTileOfflineBadgeVisibilityChanged(Tile tile) {
mSiteSectionViewHolder.updateOfflineBadge(tile);
mSnapshotTileGridChanged = true;
}
@Override
public void onEnterVr() {
mSearchBoxView.setVisibility(GONE);
}
@Override
public void onExitVr() {
mSearchBoxView.setVisibility(VISIBLE);
}
private void onDestroy() {
VrModuleProvider.unregisterVrModeObserver(this);
}
private void initializeShortcuts() {
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.SIMPLIFIED_NTP)
|| isSimplifiedNtpAblationEnabled()) {
return;
}
ViewStub shortcutsStub = findViewById(R.id.shortcuts_stub);
mShortcutsView = (ViewGroup) shortcutsStub.inflate();
mShortcutsView.findViewById(R.id.bookmarks_button)
.setOnClickListener(view -> mManager.getNavigationDelegate().navigateToBookmarks());
mShortcutsView.findViewById(R.id.downloads_button)
.setOnClickListener(
view -> mManager.getNavigationDelegate().navigateToDownloadManager());
}
/**
* Makes the Search Box and Logo as wide as Most Visited.
*/
private void unifyElementWidths() {
if (mSiteSectionView.getVisibility() != GONE) {
final int width = mSiteSectionView.getMeasuredWidth() - mTileGridLayoutBleed;
measureExactly(mSearchBoxView,
width + mSearchboxShadowWidth, mSearchBoxView.getMeasuredHeight());
measureExactly(mSearchProviderLogoView,
width, mSearchProviderLogoView.getMeasuredHeight());
} else if (mExploreSectionView != null) {
final int exploreWidth = mExploreSectionView.getMeasuredWidth() - mTileGridLayoutBleed;
measureExactly(mSearchBoxView, exploreWidth + mSearchboxShadowWidth,
mSearchBoxView.getMeasuredHeight());
measureExactly(mSearchProviderLogoView, exploreWidth,
mSearchProviderLogoView.getMeasuredHeight());
}
}
/**
* Convenience method to call measure() on the given View with MeasureSpecs converted from the
* given dimensions (in pixels) with MeasureSpec.EXACTLY.
*/
private static void measureExactly(View view, int widthPx, int heightPx) {
view.measure(MeasureSpec.makeMeasureSpec(widthPx, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(heightPx, MeasureSpec.EXACTLY));
}
/**
* Provides the additional capabilities needed for the SearchBox container layout.
*/
public static class SearchBoxContainerView extends LinearLayout {
/** Constructor for inflating from XML. */
public SearchBoxContainerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (getBackground() instanceof RippleDrawable) {
((RippleDrawable) getBackground()).setHotspot(ev.getX(), ev.getY());
}
}
return super.onInterceptTouchEvent(ev);
}
}
}