1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.chrome.browser.banners;
7 import android.animation.ObjectAnimator;
8 import android.app.Activity;
9 import android.app.PendingIntent;
10 import android.content.ActivityNotFoundException;
11 import android.content.ContentResolver;
12 import android.content.Context;
13 import android.content.Intent;
14 import android.content.IntentSender;
15 import android.content.pm.PackageManager;
16 import android.content.res.Configuration;
17 import android.content.res.Resources;
18 import android.graphics.Rect;
19 import android.os.Looper;
20 import android.util.AttributeSet;
21 import android.util.Log;
22 import android.view.LayoutInflater;
23 import android.view.MotionEvent;
24 import android.view.View;
25 import android.view.ViewConfiguration;
26 import android.view.ViewGroup;
27 import android.widget.Button;
28 import android.widget.ImageButton;
29 import android.widget.ImageView;
30 import android.widget.TextView;
32 import org.chromium.base.ApiCompatibilityUtils;
33 import org.chromium.chrome.R;
34 import org.chromium.content.browser.ContentViewCore;
35 import org.chromium.ui.base.LocalizationUtils;
36 import org.chromium.ui.base.WindowAndroid;
37 import org.chromium.ui.base.WindowAndroid.IntentCallback;
40 * Lays out a banner for showing info about an app on the Play Store.
41 * The banner mimics the appearance of a Google Now card using a background Drawable with a shadow.
43 * PADDING CALCULATIONS
44 * The banner has three different types of padding that need to be accounted for:
45 * 1) The background Drawable of the banner looks like card with a drop shadow. The Drawable
46 * defines a padding around the card that solely encompasses the space occupied by the drop
48 * 2) The card itself needs to have padding so that the widgets don't abut the borders of the card.
49 * This is defined as mPaddingCard, and is equally applied to all four sides.
50 * 3) Controls other than the icon are further constrained by mPaddingControls, which applies only
51 * to the bottom and end margins.
52 * See {@link #AppBannerView.onMeasure(int, int)} for details.
55 * Margin calculations for the banner are complicated by the background Drawable's drop shadows,
56 * since the drop shadows are meant to be counted as being part of the margin. To deal with this,
57 * the margins are calculated by deducting the background Drawable's padding from the margins
58 * defined by the XML files.
60 * EVEN MORE LAYOUT QUIRKS
61 * The layout of the banner, which includes its widget sizes, may change when the screen is rotated
62 * to account for less screen real estate. This means that all of the View's widgets and cached
63 * dimensions must be rebuilt from scratch.
65 public class AppBannerView extends SwipableOverlayView
66 implements View.OnClickListener, InstallerDelegate.Observer, IntentCallback {
67 private static final String TAG = "AppBannerView";
70 * Class that is alerted about things happening to the BannerView.
72 public static interface Observer {
74 * Called when the banner is removed from the hierarchy.
75 * @param banner Banner being dismissed.
77 public void onBannerRemoved(AppBannerView banner);
80 * Called when the user manually closes a banner.
81 * @param banner Banner being blocked.
82 * @param url URL of the page that requested the banner.
83 * @param packageName Name of the app's package.
85 public void onBannerBlocked(AppBannerView banner, String url, String packageName);
88 * Called when the banner begins to be dismissed.
89 * @param banner Banner being closed.
90 * @param dismissType Type of dismissal performed.
92 public void onBannerDismissEvent(AppBannerView banner, int dismissType);
95 * Called when an install event has occurred.
97 public void onBannerInstallEvent(AppBannerView banner, int eventType);
100 * Called when the banner needs to have an Activity started for a result.
101 * @param banner Banner firing the event.
102 * @param intent Intent to fire.
104 public boolean onFireIntent(AppBannerView banner, PendingIntent intent);
107 // Installation states.
108 private static final int INSTALL_STATE_NOT_INSTALLED = 0;
109 private static final int INSTALL_STATE_INSTALLING = 1;
110 private static final int INSTALL_STATE_INSTALLED = 2;
112 // XML layout for the BannerView.
113 private static final int BANNER_LAYOUT = R.layout.app_banner_view;
115 // True if the layout is in left-to-right layout mode (regular mode).
116 private final boolean mIsLayoutLTR;
118 // Class to alert about BannerView events.
119 private AppBannerView.Observer mObserver;
121 // Information about the package. Shouldn't ever be null after calling {@link #initialize()}.
122 private AppData mAppData;
124 // Views comprising the app banner.
125 private ImageView mIconView;
126 private TextView mTitleView;
127 private Button mInstallButtonView;
128 private RatingView mRatingView;
129 private View mLogoView;
130 private View mBannerHighlightView;
131 private ImageButton mCloseButtonView;
134 private int mDefinedMaxWidth;
135 private int mPaddingCard;
136 private int mPaddingControls;
137 private int mMarginLeft;
138 private int mMarginRight;
139 private int mMarginBottom;
140 private int mTouchSlop;
142 // Highlight variables.
143 private boolean mIsBannerPressed;
144 private float mInitialXForHighlight;
146 // Initial padding values.
147 private final Rect mBackgroundDrawablePadding;
150 private boolean mWasInstallDialogShown;
151 private InstallerDelegate mInstallTask;
152 private int mInstallState;
155 * Creates a BannerView and adds it to the given ContentViewCore.
156 * @param contentViewCore ContentViewCore to display the AppBannerView for.
157 * @param observer Class that is alerted for AppBannerView events.
158 * @param data Data about the app.
159 * @return The created banner.
161 public static AppBannerView create(
162 ContentViewCore contentViewCore, Observer observer,AppData data) {
163 Context context = contentViewCore.getContext().getApplicationContext();
164 AppBannerView banner =
165 (AppBannerView) LayoutInflater.from(context).inflate(BANNER_LAYOUT, null);
166 banner.initialize(observer, data);
167 banner.addToView(contentViewCore);
172 * Creates a BannerView from an XML layout.
174 public AppBannerView(Context context, AttributeSet attrs) {
175 super(context, attrs);
176 mIsLayoutLTR = !LocalizationUtils.isLayoutRtl();
178 // Store the background Drawable's padding. The background used for banners is a 9-patch,
179 // which means that it already defines padding. We need to take it into account when adding
180 // even more padding to the inside of it.
181 mBackgroundDrawablePadding = new Rect();
182 mBackgroundDrawablePadding.left = ApiCompatibilityUtils.getPaddingStart(this);
183 mBackgroundDrawablePadding.right = ApiCompatibilityUtils.getPaddingEnd(this);
184 mBackgroundDrawablePadding.top = getPaddingTop();
185 mBackgroundDrawablePadding.bottom = getPaddingBottom();
187 mInstallState = INSTALL_STATE_NOT_INSTALLED;
191 * Initialize the banner with information about the package.
192 * @param observer Class to alert about changes to the banner.
193 * @param data Information about the app being advertised.
195 private void initialize(Observer observer, AppData data) {
196 mObserver = observer;
198 initializeControls();
201 private void initializeControls() {
202 // Cache the banner dimensions, adjusting margins for drop shadows defined in the background
204 Resources res = getResources();
205 mDefinedMaxWidth = res.getDimensionPixelSize(R.dimen.app_banner_max_width);
206 mPaddingCard = res.getDimensionPixelSize(R.dimen.app_banner_padding);
207 mPaddingControls = res.getDimensionPixelSize(R.dimen.app_banner_padding_controls);
208 mMarginLeft = res.getDimensionPixelSize(R.dimen.app_banner_margin_sides)
209 - mBackgroundDrawablePadding.left;
210 mMarginRight = res.getDimensionPixelSize(R.dimen.app_banner_margin_sides)
211 - mBackgroundDrawablePadding.right;
212 mMarginBottom = res.getDimensionPixelSize(R.dimen.app_banner_margin_bottom)
213 - mBackgroundDrawablePadding.bottom;
214 if (getLayoutParams() != null) {
215 MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
216 params.leftMargin = mMarginLeft;
217 params.rightMargin = mMarginRight;
218 params.bottomMargin = mMarginBottom;
221 // Pull out all of the controls we are expecting.
222 mIconView = (ImageView) findViewById(R.id.app_icon);
223 mTitleView = (TextView) findViewById(R.id.app_title);
224 mInstallButtonView = (Button) findViewById(R.id.app_install_button);
225 mRatingView = (RatingView) findViewById(R.id.app_rating);
226 mLogoView = findViewById(R.id.store_logo);
227 mBannerHighlightView = findViewById(R.id.banner_highlight);
228 mCloseButtonView = (ImageButton) findViewById(R.id.close_button);
230 assert mIconView != null;
231 assert mTitleView != null;
232 assert mInstallButtonView != null;
233 assert mLogoView != null;
234 assert mRatingView != null;
235 assert mBannerHighlightView != null;
236 assert mCloseButtonView != null;
238 // Set up the buttons to fire an event.
239 mInstallButtonView.setOnClickListener(this);
240 mCloseButtonView.setOnClickListener(this);
242 // Configure the controls with the package information.
243 mTitleView.setText(mAppData.title());
244 mIconView.setImageDrawable(mAppData.icon());
245 mRatingView.initialize(mAppData.rating());
246 setAccessibilityInformation();
248 // Determine how much the user can drag sideways before their touch is considered a scroll.
249 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
251 // Set up the install button.
252 updateButtonStatus();
256 * Creates a succinct description about the app being advertised.
258 private void setAccessibilityInformation() {
259 String bannerText = getContext().getString(
260 R.string.app_banner_view_accessibility, mAppData.title(), mAppData.rating());
261 setContentDescription(bannerText);
265 public void onClick(View view) {
266 if (mObserver == null) return;
268 // Only allow the button to be clicked when the banner's in a neutral position.
269 if (Math.abs(getTranslationX()) > ZERO_THRESHOLD
270 || Math.abs(getTranslationY()) > ZERO_THRESHOLD) {
274 if (view == mInstallButtonView) {
275 // Check that nothing happened in the background to change the install state of the app.
276 int previousState = mInstallState;
277 updateButtonStatus();
278 if (mInstallState != previousState) return;
280 // Ignore button clicks when the app is installing.
281 if (mInstallState == INSTALL_STATE_INSTALLING) return;
283 mInstallButtonView.setEnabled(false);
285 if (mInstallState == INSTALL_STATE_NOT_INSTALLED) {
286 // The user initiated an install. Track it happening only once.
287 if (!mWasInstallDialogShown) {
288 mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_TRIGGERED);
289 mWasInstallDialogShown = true;
292 if (mObserver.onFireIntent(this, mAppData.installIntent())) {
293 // Temporarily hide the banner.
294 createVerticalSnapAnimation(false);
296 Log.e(TAG, "Failed to fire install intent.");
297 dismiss(AppBannerMetricsIds.DISMISS_ERROR);
299 } else if (mInstallState == INSTALL_STATE_INSTALLED) {
300 // The app is installed. Open it.
302 Intent appIntent = getAppLaunchIntent();
303 if (appIntent != null) getContext().startActivity(appIntent);
304 } catch (ActivityNotFoundException e) {
305 Log.e(TAG, "Failed to find app package: " + mAppData.packageName());
308 dismiss(AppBannerMetricsIds.DISMISS_APP_OPEN);
310 } else if (view == mCloseButtonView) {
311 if (mObserver != null) {
312 mObserver.onBannerBlocked(this, mAppData.siteUrl(), mAppData.packageName());
315 dismiss(AppBannerMetricsIds.DISMISS_CLOSE_BUTTON);
320 protected void onViewSwipedAway() {
321 if (mObserver == null) return;
322 mObserver.onBannerDismissEvent(this, AppBannerMetricsIds.DISMISS_BANNER_SWIPE);
323 mObserver.onBannerBlocked(this, mAppData.siteUrl(), mAppData.packageName());
327 protected void onViewClicked() {
328 // Send the user to the app's Play store page.
330 IntentSender sender = mAppData.detailsIntent().getIntentSender();
331 getContext().startIntentSender(sender, new Intent(), 0, 0, 0);
332 } catch (IntentSender.SendIntentException e) {
333 Log.e(TAG, "Failed to launch details intent.");
336 dismiss(AppBannerMetricsIds.DISMISS_BANNER_CLICK);
340 protected void onViewPressed(MotionEvent event) {
341 // Highlight the banner when the user has held it for long enough and doesn't move.
342 mInitialXForHighlight = event.getRawX();
343 mIsBannerPressed = true;
344 mBannerHighlightView.setVisibility(View.VISIBLE);
348 public void onIntentCompleted(WindowAndroid window, int resultCode,
349 ContentResolver contentResolver, Intent data) {
350 if (isDismissed()) return;
352 createVerticalSnapAnimation(true);
353 if (resultCode == Activity.RESULT_OK) {
354 // The user chose to install the app. Watch the PackageManager to see when it finishes
356 mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_STARTED);
358 PackageManager pm = getContext().getPackageManager();
360 new InstallerDelegate(Looper.getMainLooper(), pm, this, mAppData.packageName());
361 mInstallTask.start();
362 mInstallState = INSTALL_STATE_INSTALLING;
364 updateButtonStatus();
369 public void onInstallFinished(InstallerDelegate monitor, boolean success) {
370 if (isDismissed() || mInstallTask != monitor) return;
373 // Let the user open the app from here.
374 mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_COMPLETED);
375 mInstallState = INSTALL_STATE_INSTALLED;
376 updateButtonStatus();
378 dismiss(AppBannerMetricsIds.DISMISS_INSTALL_TIMEOUT);
383 protected ViewGroup.MarginLayoutParams createLayoutParams() {
384 // Define the margin around the entire banner that accounts for the drop shadow.
385 ViewGroup.MarginLayoutParams params = super.createLayoutParams();
386 params.setMargins(mMarginLeft, 0, mMarginRight, mMarginBottom);
391 * Removes this View from its parent and alerts any observers of the dismissal.
392 * @return Whether or not the View was successfully dismissed.
395 boolean removeFromParent() {
396 if (super.removeFromParent()) {
397 mObserver.onBannerRemoved(this);
406 * Dismisses the banner.
407 * @param eventType Event that triggered the dismissal. See {@link AppBannerMetricsIds}.
409 public void dismiss(int eventType) {
410 if (isDismissed() || mObserver == null) return;
412 dismiss(eventType == AppBannerMetricsIds.DISMISS_CLOSE_BUTTON);
413 mObserver.onBannerDismissEvent(this, eventType);
417 * Destroys the Banner.
419 public void destroy() {
420 if (!isDismissed()) dismiss(AppBannerMetricsIds.DISMISS_ERROR);
422 if (mInstallTask != null) {
423 mInstallTask.cancel();
429 * Updates the install button (install state, text, color, etc.).
431 void updateButtonStatus() {
432 if (mInstallButtonView == null) return;
434 // Determine if the saved install status of the app is out of date.
435 // It is not easily possible to detect if an app is in the process of being installed, so we
436 // can't properly transition to that state from here.
437 if (getAppLaunchIntent() == null) {
438 if (mInstallState == INSTALL_STATE_INSTALLED) {
439 mInstallState = INSTALL_STATE_NOT_INSTALLED;
442 mInstallState = INSTALL_STATE_INSTALLED;
445 // Update what the button looks like.
446 Resources res = getResources();
449 if (mInstallState == INSTALL_STATE_INSTALLED) {
450 ApiCompatibilityUtils.setBackgroundForView(mInstallButtonView,
451 res.getDrawable(R.drawable.app_banner_button_open));
452 fgColor = res.getColor(R.color.app_banner_open_button_fg);
453 text = res.getString(R.string.app_banner_open);
455 ApiCompatibilityUtils.setBackgroundForView(mInstallButtonView,
456 res.getDrawable(R.drawable.app_banner_button_install));
457 fgColor = res.getColor(R.color.app_banner_install_button_fg);
458 if (mInstallState == INSTALL_STATE_NOT_INSTALLED) {
459 text = mAppData.installButtonText();
460 mInstallButtonView.setContentDescription(
461 getContext().getString(R.string.app_banner_install_accessibility, text));
463 text = res.getString(R.string.app_banner_installing);
467 mInstallButtonView.setTextColor(fgColor);
468 mInstallButtonView.setText(text);
469 mInstallButtonView.setEnabled(mInstallState != INSTALL_STATE_INSTALLING);
473 * Determine how big an icon needs to be for the Layout.
474 * @param context Context to grab resources from.
475 * @return How big the icon is expected to be, in pixels.
477 static int getIconSize(Context context) {
478 return context.getResources().getDimensionPixelSize(R.dimen.app_banner_icon_size);
482 * Passes all touch events through to the parent.
485 public boolean onTouchEvent(MotionEvent event) {
486 int action = event.getActionMasked();
487 if (mIsBannerPressed) {
488 // Mimic Google Now card behavior, where the card stops being highlighted if the user
489 // scrolls a bit to the side.
490 float xDifference = Math.abs(event.getRawX() - mInitialXForHighlight);
491 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
492 || (action == MotionEvent.ACTION_MOVE && xDifference > mTouchSlop)) {
493 mIsBannerPressed = false;
494 mBannerHighlightView.setVisibility(View.INVISIBLE);
498 return super.onTouchEvent(event);
502 * Fade the banner back into view.
505 protected void onAttachedToWindow() {
506 super.onAttachedToWindow();
507 ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 1.f).setDuration(
508 MS_ANIMATION_DURATION).start();
509 setVisibility(VISIBLE);
513 * Immediately hide the banner to avoid having them show up in snapshots.
516 protected void onDetachedFromWindow() {
517 super.onDetachedFromWindow();
519 setVisibility(INVISIBLE);
523 * Watch for changes in the available screen height, which triggers a complete recreation of the
524 * banner widgets. This is mainly due to the fact that the Nexus 7 has a smaller banner defined
525 * for its landscape versus its portrait layouts.
528 protected void onConfigurationChanged(Configuration config) {
529 super.onConfigurationChanged(config);
531 if (isDismissed()) return;
533 // If the card's maximum width hasn't changed, the individual views can't have, either.
534 int newDefinedWidth = getResources().getDimensionPixelSize(R.dimen.app_banner_max_width);
535 if (mDefinedMaxWidth == newDefinedWidth) return;
537 // Cannibalize another version of this layout to get Views using the new resources and
539 while (getChildCount() > 0) removeViewAt(0);
542 mInstallButtonView = null;
545 mBannerHighlightView = null;
547 AppBannerView cannibalized =
548 (AppBannerView) LayoutInflater.from(getContext()).inflate(BANNER_LAYOUT, null);
549 while (cannibalized.getChildCount() > 0) {
550 View child = cannibalized.getChildAt(0);
551 cannibalized.removeViewAt(0);
554 initializeControls();
559 public void onWindowFocusChanged(boolean hasWindowFocus) {
560 if (hasWindowFocus) updateButtonStatus();
564 * @return Intent to launch the app that is being promoted.
566 private Intent getAppLaunchIntent() {
567 String packageName = mAppData.packageName();
568 PackageManager packageManager = getContext().getPackageManager();
569 return packageManager.getLaunchIntentForPackage(packageName);
573 * Measures the banner and its children Views for the given space.
575 * DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
576 * DPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPD
578 * DP...... TITLE----------------------- XcPD
580 * DP...... LOGO BUTTONcPD
581 * DP...... cccccccccccccccccccccccccccccccPD
582 * DPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPD
583 * DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
585 * The three paddings mentioned in the class Javadoc are denoted by:
586 * D) Drop shadow padding.
587 * P) Inner card padding.
588 * c) Control padding.
590 * Measurement for components of the banner are performed assuming that components are laid out
591 * inside of the banner's background as follows:
592 * 1) A maximum width is enforced on the banner to keep the whole thing on screen and keep it a
594 * 2) The icon takes up the left side of the banner.
595 * 3) The install button occupies the bottom-right of the banner.
596 * 4) The Google Play logo occupies the space to the left of the button.
597 * 5) The rating is assigned space above the logo and below the title.
598 * 6) The close button (if visible) sits in the top right of the banner.
599 * 7) The title is assigned whatever space is left and sits on top of the tallest stack of
602 * See {@link #android.view.View.onMeasure(int, int)} for the parameters.
605 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
606 // Enforce a maximum width on the banner, which is defined as the smallest of:
607 // 1) The smallest width for the device (in either landscape or portrait mode).
608 // 2) The defined maximum width in the dimens.xml files.
609 // 3) The width passed in through the MeasureSpec.
610 Resources res = getResources();
611 float density = res.getDisplayMetrics().density;
612 int screenSmallestWidth = (int) (res.getConfiguration().smallestScreenWidthDp * density);
613 int specWidth = MeasureSpec.getSize(widthMeasureSpec);
614 int bannerWidth = Math.min(Math.min(specWidth, mDefinedMaxWidth), screenSmallestWidth);
616 // Track how much space is available inside the banner's card-shaped background Drawable.
617 // To calculate this, we need to account for both the padding of the background (which
618 // is occupied by the card's drop shadows) as well as the padding defined on the inside of
620 int bgPaddingWidth = mBackgroundDrawablePadding.left + mBackgroundDrawablePadding.right;
621 int bgPaddingHeight = mBackgroundDrawablePadding.top + mBackgroundDrawablePadding.bottom;
622 final int maxControlWidth = bannerWidth - bgPaddingWidth - (mPaddingCard * 2);
624 // Control height is constrained to provide a reasonable aspect ratio.
625 // In practice, the only controls which can cause an issue are the title and the install
626 // button, since they have strings that can change size according to user preference. The
627 // other controls are all defined to be a certain height.
628 int specHeight = MeasureSpec.getSize(heightMeasureSpec);
629 int reasonableHeight = maxControlWidth / 4;
630 int paddingHeight = bgPaddingHeight + (mPaddingCard * 2);
631 final int maxControlHeight = Math.min(specHeight, reasonableHeight) - paddingHeight;
632 final int maxStackedControlHeight = maxControlWidth / 3;
634 // Determine how big each component wants to be. The icon is measured separately because
635 // it is not stacked with the other controls.
636 measureChildForSpace(mIconView, maxControlWidth, maxControlHeight);
637 for (int i = 0; i < getChildCount(); i++) {
638 if (getChildAt(i) != mIconView) {
639 measureChildForSpace(getChildAt(i), maxControlWidth, maxStackedControlHeight);
643 // Determine how tall the banner needs to be to fit everything by calculating the combined
644 // height of the stacked controls. There are three competing stacks to measure:
646 // 2) The app title + control padding + star rating + store logo.
647 // 3) The app title + control padding + install button.
648 // The control padding is extra padding that applies only to the non-icon widgets.
649 // The close button does not get counted as part of a stack.
650 int iconStackHeight = getHeightWithMargins(mIconView);
651 int logoStackHeight = getHeightWithMargins(mTitleView) + mPaddingControls
652 + getHeightWithMargins(mRatingView) + getHeightWithMargins(mLogoView);
653 int buttonStackHeight = getHeightWithMargins(mTitleView) + mPaddingControls
654 + getHeightWithMargins(mInstallButtonView);
655 int biggestStackHeight =
656 Math.max(iconStackHeight, Math.max(logoStackHeight, buttonStackHeight));
658 // The icon hugs the banner's starting edge, from the top of the banner to the bottom.
659 final int iconSize = biggestStackHeight;
660 measureChildForSpaceExactly(mIconView, iconSize, iconSize);
662 // The rest of the content is laid out to the right of the icon.
663 // Additional padding is defined for non-icon content on the end and bottom.
664 final int contentWidth =
665 maxControlWidth - getWidthWithMargins(mIconView) - mPaddingControls;
666 final int contentHeight = biggestStackHeight - mPaddingControls;
667 measureChildForSpace(mLogoView, contentWidth, contentHeight);
669 // Restrict the button size to prevent overrunning the Google Play logo.
670 int remainingButtonWidth =
671 maxControlWidth - getWidthWithMargins(mLogoView) - getWidthWithMargins(mIconView);
672 mInstallButtonView.setMaxWidth(remainingButtonWidth);
673 measureChildForSpace(mInstallButtonView, contentWidth, contentHeight);
675 // Measure the star rating, which sits below the title and above the logo.
676 final int ratingWidth = contentWidth;
677 final int ratingHeight = contentHeight - getHeightWithMargins(mLogoView);
678 measureChildForSpace(mRatingView, ratingWidth, ratingHeight);
680 // The close button sits to the right of the title and above the install button.
681 final int closeWidth = contentWidth;
682 final int closeHeight = contentHeight - getHeightWithMargins(mInstallButtonView);
683 measureChildForSpace(mCloseButtonView, closeWidth, closeHeight);
685 // The app title spans the top of the banner and sits on top of the other controls, and to
686 // the left of the close button. The computation for the width available to the title is
687 // complicated by how the button sits in the corner and absorbs the padding that would
688 // normally be there.
689 int biggerStack = Math.max(getHeightWithMargins(mInstallButtonView),
690 getHeightWithMargins(mLogoView) + getHeightWithMargins(mRatingView));
691 final int titleWidth = contentWidth - getWidthWithMargins(mCloseButtonView) + mPaddingCard;
692 final int titleHeight = contentHeight - biggerStack;
693 measureChildForSpace(mTitleView, titleWidth, titleHeight);
695 // Set the measured dimensions for the banner. The banner's height is defined by the
696 // tallest stack of components, the padding of the banner's card background, and the extra
697 // padding around the banner's components.
698 int bannerPadding = mBackgroundDrawablePadding.top + mBackgroundDrawablePadding.bottom
699 + (mPaddingCard * 2);
700 int bannerHeight = biggestStackHeight + bannerPadding;
701 setMeasuredDimension(bannerWidth, bannerHeight);
703 // Make the banner highlight view be the exact same size as the banner's card background.
704 final int cardWidth = bannerWidth - bgPaddingWidth;
705 final int cardHeight = bannerHeight - bgPaddingHeight;
706 measureChildForSpaceExactly(mBannerHighlightView, cardWidth, cardHeight);
710 * Lays out the controls according to the algorithm in {@link #onMeasure}.
711 * See {@link #android.view.View.onLayout(boolean, int, int, int, int)} for the parameters.
714 protected void onLayout(boolean changed, int l, int t, int r, int b) {
715 super.onLayout(changed, l, t, r, b);
716 int top = mBackgroundDrawablePadding.top;
717 int bottom = getMeasuredHeight() - mBackgroundDrawablePadding.bottom;
718 int start = mBackgroundDrawablePadding.left;
719 int end = getMeasuredWidth() - mBackgroundDrawablePadding.right;
721 // The highlight overlay covers the entire banner (minus drop shadow padding).
722 mBannerHighlightView.layout(start, top, end, bottom);
724 // Lay out the close button in the top-right corner. Padding that would normally go to the
725 // card is applied to the close button so that it has a bigger touch target.
726 if (mCloseButtonView.getVisibility() == VISIBLE) {
727 int closeWidth = mCloseButtonView.getMeasuredWidth();
729 top + ((MarginLayoutParams) mCloseButtonView.getLayoutParams()).topMargin;
730 int closeBottom = closeTop + mCloseButtonView.getMeasuredHeight();
731 int closeRight = mIsLayoutLTR ? end : (getMeasuredWidth() - end + closeWidth);
732 int closeLeft = closeRight - closeWidth;
733 mCloseButtonView.layout(closeLeft, closeTop, closeRight, closeBottom);
736 // Apply the padding for the rest of the widgets.
738 bottom -= mPaddingCard;
739 start += mPaddingCard;
743 int iconWidth = mIconView.getMeasuredWidth();
744 int iconLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - iconWidth);
745 mIconView.layout(iconLeft, top, iconLeft + iconWidth, top + mIconView.getMeasuredHeight());
746 start += getWidthWithMargins(mIconView);
748 // Factor in the additional padding, which is only tacked onto the end and bottom.
749 end -= mPaddingControls;
750 bottom -= mPaddingControls;
752 // Lay out the app title text.
753 int titleWidth = mTitleView.getMeasuredWidth();
754 int titleTop = top + ((MarginLayoutParams) mTitleView.getLayoutParams()).topMargin;
755 int titleBottom = titleTop + mTitleView.getMeasuredHeight();
756 int titleLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - titleWidth);
757 mTitleView.layout(titleLeft, titleTop, titleLeft + titleWidth, titleBottom);
759 // The mock shows the margin eating into the descender area of the TextView.
760 int textBaseline = mTitleView.getLineBounds(mTitleView.getLineCount() - 1, null);
761 top = titleTop + textBaseline
762 + ((MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin;
764 // Lay out the app rating below the title.
765 int starWidth = mRatingView.getMeasuredWidth();
766 int starTop = top + ((MarginLayoutParams) mRatingView.getLayoutParams()).topMargin;
767 int starBottom = starTop + mRatingView.getMeasuredHeight();
768 int starLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - starWidth);
769 mRatingView.layout(starLeft, starTop, starLeft + starWidth, starBottom);
771 // Lay out the logo in the bottom-left.
772 int logoWidth = mLogoView.getMeasuredWidth();
773 int logoBottom = bottom - ((MarginLayoutParams) mLogoView.getLayoutParams()).bottomMargin;
774 int logoTop = logoBottom - mLogoView.getMeasuredHeight();
775 int logoLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - logoWidth);
776 mLogoView.layout(logoLeft, logoTop, logoLeft + logoWidth, logoBottom);
778 // Lay out the install button in the bottom-right corner.
779 int buttonHeight = mInstallButtonView.getMeasuredHeight();
780 int buttonWidth = mInstallButtonView.getMeasuredWidth();
781 int buttonRight = mIsLayoutLTR ? end : (getMeasuredWidth() - end + buttonWidth);
782 int buttonLeft = buttonRight - buttonWidth;
783 mInstallButtonView.layout(buttonLeft, bottom - buttonHeight, buttonRight, bottom);
787 * Measures a child for the given space, accounting for defined heights and margins.
788 * @param child View to measure.
789 * @param availableWidth Available width for the view.
790 * @param availableHeight Available height for the view.
792 private void measureChildForSpace(View child, int availableWidth, int availableHeight) {
794 availableWidth -= getMarginWidth(child);
795 availableHeight -= getMarginHeight(child);
797 // Account for any layout-defined dimensions for the view.
798 int childWidth = child.getLayoutParams().width;
799 int childHeight = child.getLayoutParams().height;
800 if (childWidth >= 0) availableWidth = Math.min(availableWidth, childWidth);
801 if (childHeight >= 0) availableHeight = Math.min(availableHeight, childHeight);
803 int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST);
804 int heightSpec = MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.AT_MOST);
805 child.measure(widthSpec, heightSpec);
809 * Forces a child to exactly occupy the given space.
810 * @param child View to measure.
811 * @param availableWidth Available width for the view.
812 * @param availableHeight Available height for the view.
814 private void measureChildForSpaceExactly(View child, int availableWidth, int availableHeight) {
815 int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY);
816 int heightSpec = MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY);
817 child.measure(widthSpec, heightSpec);
821 * Calculates how wide the margins are for the given View.
822 * @param view View to measure.
823 * @return Measured width of the margins.
825 private static int getMarginWidth(View view) {
826 MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
827 return params.leftMargin + params.rightMargin;
831 * Calculates how wide the given View has been measured to be, including its margins.
832 * @param view View to measure.
833 * @return Measured width of the view plus its margins.
835 private static int getWidthWithMargins(View view) {
836 return view.getMeasuredWidth() + getMarginWidth(view);
840 * Calculates how tall the margins are for the given View.
841 * @param view View to measure.
842 * @return Measured height of the margins.
844 private static int getMarginHeight(View view) {
845 MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
846 return params.topMargin + params.bottomMargin;
850 * Calculates how tall the given View has been measured to be, including its margins.
851 * @param view View to measure.
852 * @return Measured height of the view plus its margins.
854 private static int getHeightWithMargins(View view) {
855 return view.getMeasuredHeight() + getMarginHeight(view);