// 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.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.widget.LinearLayout; import org.chromium.chrome.R; import org.chromium.chrome.browser.ntp.cards.CardsVariationParameters; import org.chromium.chrome.browser.ntp.cards.NewTabPageRecyclerView; /** * 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 { // Space permitting, the spacers will grow from 0dp to the heights given below. If there is // additional space, it will be distributed evenly between the top and bottom spacers. private static final float TOP_SPACER_HEIGHT_DP = 44f; private static final float MIDDLE_SPACER_HEIGHT_DP = 24f; private static final float BOTTOM_SPACER_HEIGHT_DP = 44f; private static final float TOTAL_SPACER_HEIGHT_DP = TOP_SPACER_HEIGHT_DP + MIDDLE_SPACER_HEIGHT_DP + BOTTOM_SPACER_HEIGHT_DP; private final int mTopSpacerIdealHeight; private final int mMiddleSpacerIdealHeight; private final int mBottomSpacerIdealHeight; private final int mTotalSpacerIdealHeight; private final int mMostVisitedLayoutBleed; private final int mPeekingCardHeight; private final int mTabStripHeight; private final int mFieldTrialLayoutAdjustment; private int mParentViewportHeight; private int mSearchboxViewShadowWidth; private boolean mCardsUiEnabled; private View mTopSpacer; // Spacer above search logo. private View mMiddleSpacer; // Spacer between toolbar and Most Likely. private View mBottomSpacer; // Spacer below Most Likely. private View mLogoSpacer; // Spacer above the logo. private View mSearchBoxSpacer; // Spacer above the search box. // Separate spacer below Most Likely to add enough space so the user can scroll with Most Likely // at the top of the screen. private View mScrollCompensationSpacer; private LogoView mSearchProviderLogoView; private View mSearchBoxView; private MostVisitedLayout mMostVisitedLayout; /** * Constructor for inflating from XML. */ public NewTabPageLayout(Context context, AttributeSet attrs) { super(context, attrs); Resources res = getResources(); float density = res.getDisplayMetrics().density; mTopSpacerIdealHeight = Math.round(density * TOP_SPACER_HEIGHT_DP); mMiddleSpacerIdealHeight = Math.round(density * MIDDLE_SPACER_HEIGHT_DP); mBottomSpacerIdealHeight = Math.round(density * BOTTOM_SPACER_HEIGHT_DP); mTotalSpacerIdealHeight = Math.round(density * TOTAL_SPACER_HEIGHT_DP); mMostVisitedLayoutBleed = res.getDimensionPixelSize(R.dimen.most_visited_layout_bleed); mPeekingCardHeight = res.getDimensionPixelSize(R.dimen.snippets_padding_and_peeking_card_height); mTabStripHeight = res.getDimensionPixelSize(R.dimen.tab_strip_height); mFieldTrialLayoutAdjustment = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CardsVariationParameters.getFirstCardOffsetDp(), res.getDisplayMetrics()); } @Override protected void onFinishInflate() { super.onFinishInflate(); mTopSpacer = findViewById(R.id.ntp_top_spacer); mMiddleSpacer = findViewById(R.id.ntp_middle_spacer); mBottomSpacer = findViewById(R.id.ntp_bottom_spacer); mLogoSpacer = findViewById(R.id.search_provider_logo_spacer); mSearchBoxSpacer = findViewById(R.id.search_box_spacer); mScrollCompensationSpacer = findViewById(R.id.ntp_scroll_spacer); mSearchProviderLogoView = (LogoView) findViewById(R.id.search_provider_logo); mSearchBoxView = findViewById(R.id.search_box); mMostVisitedLayout = (MostVisitedLayout) findViewById(R.id.most_visited_layout); setSearchBoxStyle(); } /** * Specifies the height of the parent's viewport for the container view of this View. * * As this is required in onMeasure, we can not rely on the parent having the proper * size set yet and thus must be told explicitly of this size. * * This View takes into account the presence of the tab strip height for tablets. */ public void setParentViewportHeight(int height) { mParentViewportHeight = height; } /** * Sets whether the cards UI is enabled. */ public void setUseCardsUiEnabled(boolean useCardsUi) { mCardsUiEnabled = useCardsUi; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mCardsUiEnabled) { measureWithCardsUiEnabled(widthMeasureSpec, heightMeasureSpec); } else { measureWithCardsUiDisabled(widthMeasureSpec, heightMeasureSpec); } measureCommonParts(); } /** * Performs layout measurements for when the cards ui is enabled. */ private void measureWithCardsUiEnabled(int widthMeasureSpec, int heightMeasureSpec) { assert mCardsUiEnabled; mLogoSpacer.setVisibility(View.GONE); mSearchBoxSpacer.setVisibility(View.GONE); // Remove the extra spacing before measuring because it might not be needed anymore. mMostVisitedLayout.setExtraVerticalSpacing(0); super.onMeasure(widthMeasureSpec, heightMeasureSpec); boolean hasSpaceForPeekingCard = false; int maxAboveTheFoldHeight = mParentViewportHeight - mPeekingCardHeight - mTabStripHeight - mFieldTrialLayoutAdjustment; // We need to make sure we have just enough space to show the peeking card. if (getMeasuredHeight() > maxAboveTheFoldHeight) { // We don't have enough, we will push the peeking card completely below the fold // and let MostVisited get cut to make it clear that the page is scrollable. if (mMostVisitedLayout.getChildCount() > 0) { // Add some extra space if needed. int currentBleed = getMeasuredHeight() - mParentViewportHeight; int minimumBleed = (int) (mMostVisitedLayout.getChildAt(0).getMeasuredHeight() * 0.44); if (currentBleed < minimumBleed) { int extraBleed = minimumBleed - currentBleed; mLogoSpacer.getLayoutParams().height = (int) (extraBleed * 0.25); mLogoSpacer.setVisibility(View.INVISIBLE); mSearchBoxSpacer.getLayoutParams().height = (int) (extraBleed * 0.25); mSearchBoxSpacer.setVisibility(View.INVISIBLE); mMostVisitedLayout.setExtraVerticalSpacing((int) (extraBleed * 0.5)); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } } else { hasSpaceForPeekingCard = true; // We leave more than or just enough space needed for the peeking card. Redistribute // any weighted space. // Call super.onMeasure with mode EXACTLY and the target height to allow the top // spacer (which has a weight of 1) to grow and take up the remaining space. heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxAboveTheFoldHeight, MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightMeasureSpec); distributeExtraSpace(mTopSpacer.getMeasuredHeight()); } assert getParent() instanceof NewTabPageRecyclerView; NewTabPageRecyclerView recyclerView = (NewTabPageRecyclerView) getParent(); recyclerView.setHasSpaceForPeekingCard(hasSpaceForPeekingCard); } /** * Performs layout measurements for when the cards ui is disabled. */ private void measureWithCardsUiDisabled(int widthMeasureSpec, int heightMeasureSpec) { assert !mCardsUiEnabled; // Remove the scroll spacer from the layout so the weighted children can be measured // correctly. mScrollCompensationSpacer.setVisibility(View.GONE); super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getMeasuredHeight() > mParentViewportHeight) { // This layout is bigger than its parent's viewport, so the user will need to scroll // to see all of it. Extra spacing should be added at the bottom so the user can // scroll until Most Visited is at the top. // The top, middle, and bottom spacers should have a measured height of 0 at this // point since they use weights to set height, and there was no extra space. assert mTopSpacer.getMeasuredHeight() == 0; assert mMiddleSpacer.getMeasuredHeight() == 0; assert mBottomSpacer.getMeasuredHeight() == 0; final int topOfMostVisited = calculateTopOfMostVisited(); final int belowTheFoldHeight = getMeasuredHeight() - mParentViewportHeight; if (belowTheFoldHeight < topOfMostVisited) { // Include the scroll spacer in the layout and call super.onMeasure again so it // is measured. mScrollCompensationSpacer.getLayoutParams().height = topOfMostVisited - belowTheFoldHeight; mScrollCompensationSpacer.setVisibility(View.INVISIBLE); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } else { distributeExtraSpace(mTopSpacer.getMeasuredHeight()); } } /** * Performs measurements that should be done whether the cards ui is enabled or not. */ private void measureCommonParts() { // Make the search box and logo as wide as the most visited items. if (mMostVisitedLayout.getVisibility() != GONE) { final int width = mMostVisitedLayout.getMeasuredWidth() - mMostVisitedLayoutBleed; measureExactly(mSearchBoxView, width + mSearchboxViewShadowWidth, mSearchBoxView.getMeasuredHeight()); measureExactly( mSearchProviderLogoView, width, mSearchProviderLogoView.getMeasuredHeight()); } } /** * Calculate the vertical position of Most Visited. * This method does not use mMostVisitedLayout.getTop(), so can be called in onMeasure. */ private int calculateTopOfMostVisited() { // Manually add the heights (and margins) of all children above Most Visited. int top = 0; int mostVisitedIndex = indexOfChild(mMostVisitedLayout); for (int i = 0; i < mostVisitedIndex; i++) { View child = getChildAt(i); if (child.getVisibility() == View.GONE) continue; MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams(); top += params.topMargin + child.getMeasuredHeight() + params.bottomMargin; } top += ((MarginLayoutParams) mMostVisitedLayout.getLayoutParams()).topMargin; return top; } /** * Set the search box style, adding a shadow if required. */ private void setSearchBoxStyle() { if (!NtpStyleUtils.shouldUseMaterialDesign()) return; Resources resources = getContext().getResources(); // Adjust the margins to account for the bigger size due to the shadow. MarginLayoutParams layoutParams = (MarginLayoutParams) mSearchBoxView.getLayoutParams(); layoutParams.setMargins( resources.getDimensionPixelSize(R.dimen.ntp_search_box_material_margin_left), resources.getDimensionPixelSize(R.dimen.ntp_search_box_material_margin_top), resources.getDimensionPixelSize(R.dimen.ntp_search_box_material_margin_right), resources.getDimensionPixelSize(R.dimen.ntp_search_box_material_margin_bottom)); layoutParams.height = resources .getDimensionPixelSize(R.dimen.ntp_search_box_material_height); // Width will be adjusted in onMeasure(); mSearchboxViewShadowWidth = resources .getDimensionPixelOffset(R.dimen.ntp_search_box_material_extra_width); mSearchBoxView.setBackgroundResource(R.drawable.textbox); mSearchBoxView.setPadding( resources.getDimensionPixelSize(R.dimen.ntp_search_box_material_padding_left), resources.getDimensionPixelSize(R.dimen.ntp_search_box_material_padding_top), resources.getDimensionPixelSize(R.dimen.ntp_search_box_material_padding_right), resources.getDimensionPixelSize(R.dimen.ntp_search_box_material_padding_bottom)); } /** * Distribute extra vertical space between the three spacer views. Doing this here allows for * more sophisticated constraints than in xml. * @param extraHeight The amount of extra space, in pixels. */ private void distributeExtraSpace(int extraHeight) { int topSpacerHeight; int middleSpacerHeight; int bottomSpacerHeight; if (extraHeight < mTotalSpacerIdealHeight) { // The spacers will be less than their ideal height, shrink them proportionally. topSpacerHeight = Math.round(extraHeight * (TOP_SPACER_HEIGHT_DP / TOTAL_SPACER_HEIGHT_DP)); middleSpacerHeight = Math.round(extraHeight * (MIDDLE_SPACER_HEIGHT_DP / TOTAL_SPACER_HEIGHT_DP)); bottomSpacerHeight = extraHeight - topSpacerHeight - middleSpacerHeight; } else { // Distribute remaining space evenly between the top and bottom spacers. extraHeight -= mTotalSpacerIdealHeight; topSpacerHeight = mTopSpacerIdealHeight + extraHeight / 2; middleSpacerHeight = mMiddleSpacerIdealHeight; bottomSpacerHeight = mBottomSpacerIdealHeight + extraHeight / 2; } measureExactly(mTopSpacer, 0, topSpacerHeight); measureExactly(mMiddleSpacer, 0, middleSpacerHeight); measureExactly(mBottomSpacer, 0, bottomSpacerHeight); } /** * 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)); } }