// Copyright 2014 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.banners; import android.animation.ObjectAnimator; import android.app.Activity; import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.os.Looper; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.chrome.R; import org.chromium.content.browser.ContentViewCore; import org.chromium.ui.base.LocalizationUtils; import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid.IntentCallback; /** * Lays out a banner for showing info about an app on the Play Store. * The banner mimics the appearance of a Google Now card using a background Drawable with a shadow. * * PADDING CALCULATIONS * The banner has three different types of padding that need to be accounted for: * 1) The background Drawable of the banner looks like card with a drop shadow. The Drawable * defines a padding around the card that solely encompasses the space occupied by the drop * shadow. * 2) The card itself needs to have padding so that the widgets don't abut the borders of the card. * This is defined as mPaddingCard, and is equally applied to all four sides. * 3) Controls other than the icon are further constrained by mPaddingControls, which applies only * to the bottom and end margins. * See {@link #AppBannerView.onMeasure(int, int)} for details. * * MARGIN CALCULATIONS * Margin calculations for the banner are complicated by the background Drawable's drop shadows, * since the drop shadows are meant to be counted as being part of the margin. To deal with this, * the margins are calculated by deducting the background Drawable's padding from the margins * defined by the XML files. * * EVEN MORE LAYOUT QUIRKS * The layout of the banner, which includes its widget sizes, may change when the screen is rotated * to account for less screen real estate. This means that all of the View's widgets and cached * dimensions must be rebuilt from scratch. */ public class AppBannerView extends SwipableOverlayView implements View.OnClickListener, InstallerDelegate.Observer, IntentCallback { private static final String TAG = "AppBannerView"; /** * Class that is alerted about things happening to the BannerView. */ public static interface Observer { /** * Called when the banner is removed from the hierarchy. * @param banner Banner being dismissed. */ public void onBannerRemoved(AppBannerView banner); /** * Called when the user manually closes a banner. * @param banner Banner being blocked. * @param url URL of the page that requested the banner. * @param packageName Name of the app's package. */ public void onBannerBlocked(AppBannerView banner, String url, String packageName); /** * Called when the banner begins to be dismissed. * @param banner Banner being closed. * @param dismissType Type of dismissal performed. */ public void onBannerDismissEvent(AppBannerView banner, int dismissType); /** * Called when an install event has occurred. */ public void onBannerInstallEvent(AppBannerView banner, int eventType); /** * Called when the banner needs to have an Activity started for a result. * @param banner Banner firing the event. * @param intent Intent to fire. */ public boolean onFireIntent(AppBannerView banner, PendingIntent intent); } // Installation states. private static final int INSTALL_STATE_NOT_INSTALLED = 0; private static final int INSTALL_STATE_INSTALLING = 1; private static final int INSTALL_STATE_INSTALLED = 2; // XML layout for the BannerView. private static final int BANNER_LAYOUT = R.layout.app_banner_view; // True if the layout is in left-to-right layout mode (regular mode). private final boolean mIsLayoutLTR; // Class to alert about BannerView events. private AppBannerView.Observer mObserver; // Information about the package. Shouldn't ever be null after calling {@link #initialize()}. private AppData mAppData; // Views comprising the app banner. private ImageView mIconView; private TextView mTitleView; private Button mInstallButtonView; private RatingView mRatingView; private View mLogoView; private View mBannerHighlightView; private ImageButton mCloseButtonView; // Dimension values. private int mDefinedMaxWidth; private int mPaddingCard; private int mPaddingControls; private int mMarginLeft; private int mMarginRight; private int mMarginBottom; private int mTouchSlop; // Highlight variables. private boolean mIsBannerPressed; private float mInitialXForHighlight; // Initial padding values. private final Rect mBackgroundDrawablePadding; // Install tracking. private boolean mWasInstallDialogShown; private InstallerDelegate mInstallTask; private int mInstallState; /** * Creates a BannerView and adds it to the given ContentViewCore. * @param contentViewCore ContentViewCore to display the AppBannerView for. * @param observer Class that is alerted for AppBannerView events. * @param data Data about the app. * @return The created banner. */ public static AppBannerView create( ContentViewCore contentViewCore, Observer observer, AppData data) { Context context = contentViewCore.getContext().getApplicationContext(); AppBannerView banner = (AppBannerView) LayoutInflater.from(context).inflate(BANNER_LAYOUT, null); banner.initialize(observer, data); banner.addToView(contentViewCore); return banner; } /** * Creates a BannerView from an XML layout. */ public AppBannerView(Context context, AttributeSet attrs) { super(context, attrs); mIsLayoutLTR = !LocalizationUtils.isLayoutRtl(); // Store the background Drawable's padding. The background used for banners is a 9-patch, // which means that it already defines padding. We need to take it into account when adding // even more padding to the inside of it. mBackgroundDrawablePadding = new Rect(); mBackgroundDrawablePadding.left = ApiCompatibilityUtils.getPaddingStart(this); mBackgroundDrawablePadding.right = ApiCompatibilityUtils.getPaddingEnd(this); mBackgroundDrawablePadding.top = getPaddingTop(); mBackgroundDrawablePadding.bottom = getPaddingBottom(); mInstallState = INSTALL_STATE_NOT_INSTALLED; } /** * Initialize the banner with information about the package. * @param observer Class to alert about changes to the banner. * @param data Information about the app being advertised. */ private void initialize(Observer observer, AppData data) { mObserver = observer; mAppData = data; initializeControls(); } private void initializeControls() { // Cache the banner dimensions, adjusting margins for drop shadows defined in the background // Drawable. Resources res = getResources(); mDefinedMaxWidth = res.getDimensionPixelSize(R.dimen.app_banner_max_width); mPaddingCard = res.getDimensionPixelSize(R.dimen.app_banner_padding); mPaddingControls = res.getDimensionPixelSize(R.dimen.app_banner_padding_controls); mMarginLeft = res.getDimensionPixelSize(R.dimen.app_banner_margin_sides) - mBackgroundDrawablePadding.left; mMarginRight = res.getDimensionPixelSize(R.dimen.app_banner_margin_sides) - mBackgroundDrawablePadding.right; mMarginBottom = res.getDimensionPixelSize(R.dimen.app_banner_margin_bottom) - mBackgroundDrawablePadding.bottom; if (getLayoutParams() != null) { MarginLayoutParams params = (MarginLayoutParams) getLayoutParams(); params.leftMargin = mMarginLeft; params.rightMargin = mMarginRight; params.bottomMargin = mMarginBottom; } // Pull out all of the controls we are expecting. mIconView = (ImageView) findViewById(R.id.app_icon); mTitleView = (TextView) findViewById(R.id.app_title); mInstallButtonView = (Button) findViewById(R.id.app_install_button); mRatingView = (RatingView) findViewById(R.id.app_rating); mLogoView = findViewById(R.id.store_logo); mBannerHighlightView = findViewById(R.id.banner_highlight); mCloseButtonView = (ImageButton) findViewById(R.id.close_button); assert mIconView != null; assert mTitleView != null; assert mInstallButtonView != null; assert mLogoView != null; assert mRatingView != null; assert mBannerHighlightView != null; assert mCloseButtonView != null; // Set up the buttons to fire an event. mInstallButtonView.setOnClickListener(this); mCloseButtonView.setOnClickListener(this); // Configure the controls with the package information. mTitleView.setText(mAppData.title()); mIconView.setImageDrawable(mAppData.icon()); mRatingView.initialize(mAppData.rating()); setAccessibilityInformation(); // Determine how much the user can drag sideways before their touch is considered a scroll. mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); // Set up the install button. updateButtonStatus(); } /** * Creates a succinct description about the app being advertised. */ private void setAccessibilityInformation() { String bannerText = getContext().getString( R.string.app_banner_view_accessibility, mAppData.title(), mAppData.rating()); setContentDescription(bannerText); } @Override public void onClick(View view) { if (mObserver == null) return; // Only allow the button to be clicked when the banner's in a neutral position. if (Math.abs(getTranslationX()) > ZERO_THRESHOLD || Math.abs(getTranslationY()) > ZERO_THRESHOLD) { return; } if (view == mInstallButtonView) { // Check that nothing happened in the background to change the install state of the app. int previousState = mInstallState; updateButtonStatus(); if (mInstallState != previousState) return; // Ignore button clicks when the app is installing. if (mInstallState == INSTALL_STATE_INSTALLING) return; mInstallButtonView.setEnabled(false); if (mInstallState == INSTALL_STATE_NOT_INSTALLED) { // The user initiated an install. Track it happening only once. if (!mWasInstallDialogShown) { mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_TRIGGERED); mWasInstallDialogShown = true; } if (mObserver.onFireIntent(this, mAppData.installIntent())) { // Temporarily hide the banner. createVerticalSnapAnimation(false); } else { Log.e(TAG, "Failed to fire install intent."); dismiss(AppBannerMetricsIds.DISMISS_ERROR); } } else if (mInstallState == INSTALL_STATE_INSTALLED) { // The app is installed. Open it. try { Intent appIntent = getAppLaunchIntent(); if (appIntent != null) getContext().startActivity(appIntent); } catch (ActivityNotFoundException e) { Log.e(TAG, "Failed to find app package: " + mAppData.packageName()); } dismiss(AppBannerMetricsIds.DISMISS_APP_OPEN); } } else if (view == mCloseButtonView) { if (mObserver != null) { mObserver.onBannerBlocked(this, mAppData.siteUrl(), mAppData.packageName()); } dismiss(AppBannerMetricsIds.DISMISS_CLOSE_BUTTON); } } @Override protected void onViewSwipedAway() { if (mObserver == null) return; mObserver.onBannerDismissEvent(this, AppBannerMetricsIds.DISMISS_BANNER_SWIPE); mObserver.onBannerBlocked(this, mAppData.siteUrl(), mAppData.packageName()); } @Override protected void onViewClicked() { // Send the user to the app's Play store page. try { IntentSender sender = mAppData.detailsIntent().getIntentSender(); getContext().startIntentSender(sender, new Intent(), 0, 0, 0); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "Failed to launch details intent."); } dismiss(AppBannerMetricsIds.DISMISS_BANNER_CLICK); } @Override protected void onViewPressed(MotionEvent event) { // Highlight the banner when the user has held it for long enough and doesn't move. mInitialXForHighlight = event.getRawX(); mIsBannerPressed = true; mBannerHighlightView.setVisibility(View.VISIBLE); } @Override public void onIntentCompleted(WindowAndroid window, int resultCode, ContentResolver contentResolver, Intent data) { if (isDismissed()) return; createVerticalSnapAnimation(true); if (resultCode == Activity.RESULT_OK) { // The user chose to install the app. Watch the PackageManager to see when it finishes // installing it. mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_STARTED); PackageManager pm = getContext().getPackageManager(); mInstallTask = new InstallerDelegate(Looper.getMainLooper(), pm, this, mAppData.packageName()); mInstallTask.start(); mInstallState = INSTALL_STATE_INSTALLING; } updateButtonStatus(); } @Override public void onInstallFinished(InstallerDelegate monitor, boolean success) { if (isDismissed() || mInstallTask != monitor) return; if (success) { // Let the user open the app from here. mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_COMPLETED); mInstallState = INSTALL_STATE_INSTALLED; updateButtonStatus(); } else { dismiss(AppBannerMetricsIds.DISMISS_INSTALL_TIMEOUT); } } @Override protected ViewGroup.MarginLayoutParams createLayoutParams() { // Define the margin around the entire banner that accounts for the drop shadow. ViewGroup.MarginLayoutParams params = super.createLayoutParams(); params.setMargins(mMarginLeft, 0, mMarginRight, mMarginBottom); return params; } /** * Removes this View from its parent and alerts any observers of the dismissal. * @return Whether or not the View was successfully dismissed. */ @Override boolean removeFromParent() { if (super.removeFromParent()) { mObserver.onBannerRemoved(this); destroy(); return true; } return false; } /** * Dismisses the banner. * @param eventType Event that triggered the dismissal. See {@link AppBannerMetricsIds}. */ public void dismiss(int eventType) { if (isDismissed() || mObserver == null) return; dismiss(eventType == AppBannerMetricsIds.DISMISS_CLOSE_BUTTON); mObserver.onBannerDismissEvent(this, eventType); } /** * Destroys the Banner. */ public void destroy() { if (!isDismissed()) dismiss(AppBannerMetricsIds.DISMISS_ERROR); if (mInstallTask != null) { mInstallTask.cancel(); mInstallTask = null; } } /** * Updates the install button (install state, text, color, etc.). */ void updateButtonStatus() { if (mInstallButtonView == null) return; // Determine if the saved install status of the app is out of date. // It is not easily possible to detect if an app is in the process of being installed, so we // can't properly transition to that state from here. if (getAppLaunchIntent() == null) { if (mInstallState == INSTALL_STATE_INSTALLED) { mInstallState = INSTALL_STATE_NOT_INSTALLED; } } else { mInstallState = INSTALL_STATE_INSTALLED; } // Update what the button looks like. Resources res = getResources(); int fgColor; String text; if (mInstallState == INSTALL_STATE_INSTALLED) { ApiCompatibilityUtils.setBackgroundForView(mInstallButtonView, res.getDrawable(R.drawable.app_banner_button_open)); fgColor = res.getColor(R.color.app_banner_open_button_fg); text = res.getString(R.string.app_banner_open); } else { ApiCompatibilityUtils.setBackgroundForView(mInstallButtonView, res.getDrawable(R.drawable.app_banner_button_install)); fgColor = res.getColor(R.color.app_banner_install_button_fg); if (mInstallState == INSTALL_STATE_NOT_INSTALLED) { text = mAppData.installButtonText(); mInstallButtonView.setContentDescription( getContext().getString(R.string.app_banner_install_accessibility, text)); } else { text = res.getString(R.string.app_banner_installing); } } mInstallButtonView.setTextColor(fgColor); mInstallButtonView.setText(text); mInstallButtonView.setEnabled(mInstallState != INSTALL_STATE_INSTALLING); } /** * Determine how big an icon needs to be for the Layout. * @param context Context to grab resources from. * @return How big the icon is expected to be, in pixels. */ static int getIconSize(Context context) { return context.getResources().getDimensionPixelSize(R.dimen.app_banner_icon_size); } /** * Passes all touch events through to the parent. */ @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getActionMasked(); if (mIsBannerPressed) { // Mimic Google Now card behavior, where the card stops being highlighted if the user // scrolls a bit to the side. float xDifference = Math.abs(event.getRawX() - mInitialXForHighlight); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL || (action == MotionEvent.ACTION_MOVE && xDifference > mTouchSlop)) { mIsBannerPressed = false; mBannerHighlightView.setVisibility(View.INVISIBLE); } } return super.onTouchEvent(event); } /** * Fade the banner back into view. */ @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 1.f).setDuration( MS_ANIMATION_DURATION).start(); setVisibility(VISIBLE); } /** * Immediately hide the banner to avoid having them show up in snapshots. */ @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); setAlpha(0.0f); setVisibility(INVISIBLE); } /** * Watch for changes in the available screen height, which triggers a complete recreation of the * banner widgets. This is mainly due to the fact that the Nexus 7 has a smaller banner defined * for its landscape versus its portrait layouts. */ @Override protected void onConfigurationChanged(Configuration config) { super.onConfigurationChanged(config); if (isDismissed()) return; // If the card's maximum width hasn't changed, the individual views can't have, either. int newDefinedWidth = getResources().getDimensionPixelSize(R.dimen.app_banner_max_width); if (mDefinedMaxWidth == newDefinedWidth) return; // Cannibalize another version of this layout to get Views using the new resources and // sizes. while (getChildCount() > 0) removeViewAt(0); mIconView = null; mTitleView = null; mInstallButtonView = null; mRatingView = null; mLogoView = null; mBannerHighlightView = null; AppBannerView cannibalized = (AppBannerView) LayoutInflater.from(getContext()).inflate(BANNER_LAYOUT, null); while (cannibalized.getChildCount() > 0) { View child = cannibalized.getChildAt(0); cannibalized.removeViewAt(0); addView(child); } initializeControls(); requestLayout(); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if (hasWindowFocus) updateButtonStatus(); } /** * @return Intent to launch the app that is being promoted. */ private Intent getAppLaunchIntent() { String packageName = mAppData.packageName(); PackageManager packageManager = getContext().getPackageManager(); return packageManager.getLaunchIntentForPackage(packageName); } /** * Measures the banner and its children Views for the given space. * * DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD * DPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPD * DP...... cPD * DP...... TITLE----------------------- XcPD * DP.ICON. ***** cPD * DP...... LOGO BUTTONcPD * DP...... cccccccccccccccccccccccccccccccPD * DPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPD * DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD * * The three paddings mentioned in the class Javadoc are denoted by: * D) Drop shadow padding. * P) Inner card padding. * c) Control padding. * * Measurement for components of the banner are performed assuming that components are laid out * inside of the banner's background as follows: * 1) A maximum width is enforced on the banner to keep the whole thing on screen and keep it a * reasonable size. * 2) The icon takes up the left side of the banner. * 3) The install button occupies the bottom-right of the banner. * 4) The Google Play logo occupies the space to the left of the button. * 5) The rating is assigned space above the logo and below the title. * 6) The close button (if visible) sits in the top right of the banner. * 7) The title is assigned whatever space is left and sits on top of the tallest stack of * controls. * * See {@link #android.view.View.onMeasure(int, int)} for the parameters. */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Enforce a maximum width on the banner, which is defined as the smallest of: // 1) The smallest width for the device (in either landscape or portrait mode). // 2) The defined maximum width in the dimens.xml files. // 3) The width passed in through the MeasureSpec. Resources res = getResources(); float density = res.getDisplayMetrics().density; int screenSmallestWidth = (int) (res.getConfiguration().smallestScreenWidthDp * density); int specWidth = MeasureSpec.getSize(widthMeasureSpec); int bannerWidth = Math.min(Math.min(specWidth, mDefinedMaxWidth), screenSmallestWidth); // Track how much space is available inside the banner's card-shaped background Drawable. // To calculate this, we need to account for both the padding of the background (which // is occupied by the card's drop shadows) as well as the padding defined on the inside of // the card. int bgPaddingWidth = mBackgroundDrawablePadding.left + mBackgroundDrawablePadding.right; int bgPaddingHeight = mBackgroundDrawablePadding.top + mBackgroundDrawablePadding.bottom; final int maxControlWidth = bannerWidth - bgPaddingWidth - (mPaddingCard * 2); // Control height is constrained to provide a reasonable aspect ratio. // In practice, the only controls which can cause an issue are the title and the install // button, since they have strings that can change size according to user preference. The // other controls are all defined to be a certain height. int specHeight = MeasureSpec.getSize(heightMeasureSpec); int reasonableHeight = maxControlWidth / 4; int paddingHeight = bgPaddingHeight + (mPaddingCard * 2); final int maxControlHeight = Math.min(specHeight, reasonableHeight) - paddingHeight; final int maxStackedControlHeight = maxControlWidth / 3; // Determine how big each component wants to be. The icon is measured separately because // it is not stacked with the other controls. measureChildForSpace(mIconView, maxControlWidth, maxControlHeight); for (int i = 0; i < getChildCount(); i++) { if (getChildAt(i) != mIconView) { measureChildForSpace(getChildAt(i), maxControlWidth, maxStackedControlHeight); } } // Determine how tall the banner needs to be to fit everything by calculating the combined // height of the stacked controls. There are three competing stacks to measure: // 1) The icon. // 2) The app title + control padding + star rating + store logo. // 3) The app title + control padding + install button. // The control padding is extra padding that applies only to the non-icon widgets. // The close button does not get counted as part of a stack. int iconStackHeight = getHeightWithMargins(mIconView); int logoStackHeight = getHeightWithMargins(mTitleView) + mPaddingControls + getHeightWithMargins(mRatingView) + getHeightWithMargins(mLogoView); int buttonStackHeight = getHeightWithMargins(mTitleView) + mPaddingControls + getHeightWithMargins(mInstallButtonView); int biggestStackHeight = Math.max(iconStackHeight, Math.max(logoStackHeight, buttonStackHeight)); // The icon hugs the banner's starting edge, from the top of the banner to the bottom. final int iconSize = biggestStackHeight; measureChildForSpaceExactly(mIconView, iconSize, iconSize); // The rest of the content is laid out to the right of the icon. // Additional padding is defined for non-icon content on the end and bottom. final int contentWidth = maxControlWidth - getWidthWithMargins(mIconView) - mPaddingControls; final int contentHeight = biggestStackHeight - mPaddingControls; measureChildForSpace(mLogoView, contentWidth, contentHeight); // Restrict the button size to prevent overrunning the Google Play logo. int remainingButtonWidth = maxControlWidth - getWidthWithMargins(mLogoView) - getWidthWithMargins(mIconView); mInstallButtonView.setMaxWidth(remainingButtonWidth); measureChildForSpace(mInstallButtonView, contentWidth, contentHeight); // Measure the star rating, which sits below the title and above the logo. final int ratingWidth = contentWidth; final int ratingHeight = contentHeight - getHeightWithMargins(mLogoView); measureChildForSpace(mRatingView, ratingWidth, ratingHeight); // The close button sits to the right of the title and above the install button. final int closeWidth = contentWidth; final int closeHeight = contentHeight - getHeightWithMargins(mInstallButtonView); measureChildForSpace(mCloseButtonView, closeWidth, closeHeight); // The app title spans the top of the banner and sits on top of the other controls, and to // the left of the close button. The computation for the width available to the title is // complicated by how the button sits in the corner and absorbs the padding that would // normally be there. int biggerStack = Math.max(getHeightWithMargins(mInstallButtonView), getHeightWithMargins(mLogoView) + getHeightWithMargins(mRatingView)); final int titleWidth = contentWidth - getWidthWithMargins(mCloseButtonView) + mPaddingCard; final int titleHeight = contentHeight - biggerStack; measureChildForSpace(mTitleView, titleWidth, titleHeight); // Set the measured dimensions for the banner. The banner's height is defined by the // tallest stack of components, the padding of the banner's card background, and the extra // padding around the banner's components. int bannerPadding = mBackgroundDrawablePadding.top + mBackgroundDrawablePadding.bottom + (mPaddingCard * 2); int bannerHeight = biggestStackHeight + bannerPadding; setMeasuredDimension(bannerWidth, bannerHeight); // Make the banner highlight view be the exact same size as the banner's card background. final int cardWidth = bannerWidth - bgPaddingWidth; final int cardHeight = bannerHeight - bgPaddingHeight; measureChildForSpaceExactly(mBannerHighlightView, cardWidth, cardHeight); } /** * Lays out the controls according to the algorithm in {@link #onMeasure}. * See {@link #android.view.View.onLayout(boolean, int, int, int, int)} for the parameters. */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); int top = mBackgroundDrawablePadding.top; int bottom = getMeasuredHeight() - mBackgroundDrawablePadding.bottom; int start = mBackgroundDrawablePadding.left; int end = getMeasuredWidth() - mBackgroundDrawablePadding.right; // The highlight overlay covers the entire banner (minus drop shadow padding). mBannerHighlightView.layout(start, top, end, bottom); // Lay out the close button in the top-right corner. Padding that would normally go to the // card is applied to the close button so that it has a bigger touch target. if (mCloseButtonView.getVisibility() == VISIBLE) { int closeWidth = mCloseButtonView.getMeasuredWidth(); int closeTop = top + ((MarginLayoutParams) mCloseButtonView.getLayoutParams()).topMargin; int closeBottom = closeTop + mCloseButtonView.getMeasuredHeight(); int closeRight = mIsLayoutLTR ? end : (getMeasuredWidth() - end + closeWidth); int closeLeft = closeRight - closeWidth; mCloseButtonView.layout(closeLeft, closeTop, closeRight, closeBottom); } // Apply the padding for the rest of the widgets. top += mPaddingCard; bottom -= mPaddingCard; start += mPaddingCard; end -= mPaddingCard; // Lay out the icon. int iconWidth = mIconView.getMeasuredWidth(); int iconLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - iconWidth); mIconView.layout(iconLeft, top, iconLeft + iconWidth, top + mIconView.getMeasuredHeight()); start += getWidthWithMargins(mIconView); // Factor in the additional padding, which is only tacked onto the end and bottom. end -= mPaddingControls; bottom -= mPaddingControls; // Lay out the app title text. int titleWidth = mTitleView.getMeasuredWidth(); int titleTop = top + ((MarginLayoutParams) mTitleView.getLayoutParams()).topMargin; int titleBottom = titleTop + mTitleView.getMeasuredHeight(); int titleLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - titleWidth); mTitleView.layout(titleLeft, titleTop, titleLeft + titleWidth, titleBottom); // The mock shows the margin eating into the descender area of the TextView. int textBaseline = mTitleView.getLineBounds(mTitleView.getLineCount() - 1, null); top = titleTop + textBaseline + ((MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin; // Lay out the app rating below the title. int starWidth = mRatingView.getMeasuredWidth(); int starTop = top + ((MarginLayoutParams) mRatingView.getLayoutParams()).topMargin; int starBottom = starTop + mRatingView.getMeasuredHeight(); int starLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - starWidth); mRatingView.layout(starLeft, starTop, starLeft + starWidth, starBottom); // Lay out the logo in the bottom-left. int logoWidth = mLogoView.getMeasuredWidth(); int logoBottom = bottom - ((MarginLayoutParams) mLogoView.getLayoutParams()).bottomMargin; int logoTop = logoBottom - mLogoView.getMeasuredHeight(); int logoLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - logoWidth); mLogoView.layout(logoLeft, logoTop, logoLeft + logoWidth, logoBottom); // Lay out the install button in the bottom-right corner. int buttonHeight = mInstallButtonView.getMeasuredHeight(); int buttonWidth = mInstallButtonView.getMeasuredWidth(); int buttonRight = mIsLayoutLTR ? end : (getMeasuredWidth() - end + buttonWidth); int buttonLeft = buttonRight - buttonWidth; mInstallButtonView.layout(buttonLeft, bottom - buttonHeight, buttonRight, bottom); } /** * Measures a child for the given space, accounting for defined heights and margins. * @param child View to measure. * @param availableWidth Available width for the view. * @param availableHeight Available height for the view. */ private void measureChildForSpace(View child, int availableWidth, int availableHeight) { // Handle margins. availableWidth -= getMarginWidth(child); availableHeight -= getMarginHeight(child); // Account for any layout-defined dimensions for the view. int childWidth = child.getLayoutParams().width; int childHeight = child.getLayoutParams().height; if (childWidth >= 0) availableWidth = Math.min(availableWidth, childWidth); if (childHeight >= 0) availableHeight = Math.min(availableHeight, childHeight); int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST); int heightSpec = MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.AT_MOST); child.measure(widthSpec, heightSpec); } /** * Forces a child to exactly occupy the given space. * @param child View to measure. * @param availableWidth Available width for the view. * @param availableHeight Available height for the view. */ private void measureChildForSpaceExactly(View child, int availableWidth, int availableHeight) { int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY); int heightSpec = MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY); child.measure(widthSpec, heightSpec); } /** * Calculates how wide the margins are for the given View. * @param view View to measure. * @return Measured width of the margins. */ private static int getMarginWidth(View view) { MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams(); return params.leftMargin + params.rightMargin; } /** * Calculates how wide the given View has been measured to be, including its margins. * @param view View to measure. * @return Measured width of the view plus its margins. */ private static int getWidthWithMargins(View view) { return view.getMeasuredWidth() + getMarginWidth(view); } /** * Calculates how tall the margins are for the given View. * @param view View to measure. * @return Measured height of the margins. */ private static int getMarginHeight(View view) { MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams(); return params.topMargin + params.bottomMargin; } /** * Calculates how tall the given View has been measured to be, including its margins. * @param view View to measure. * @return Measured height of the view plus its margins. */ private static int getHeightWithMargins(View view) { return view.getMeasuredHeight() + getMarginHeight(view); } }