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.content.Context;
9 import android.content.res.Resources;
10 import android.graphics.Point;
11 import android.graphics.drawable.ClipDrawable;
12 import android.graphics.drawable.Drawable;
13 import android.util.AttributeSet;
14 import android.view.Gravity;
15 import android.view.LayoutInflater;
16 import android.view.View;
17 import android.widget.Button;
18 import android.widget.ImageView;
19 import android.widget.TextView;
21 import org.chromium.base.ApiCompatibilityUtils;
22 import org.chromium.chrome.R;
23 import org.chromium.content.browser.ContentView;
24 import org.chromium.ui.base.LocalizationUtils;
27 * Lays out a banner for showing info about an app on the Play Store.
28 * Rather than utilizing the Android RatingBar, which would require some nasty styling, a custom
29 * View is used to paint a Drawable showing all the stars. Showing different ratings is done by
30 * adjusting the clipping rectangle width.
32 public class AppBannerView extends SwipableOverlayView implements View.OnClickListener {
34 * Class that is alerted about things happening to the BannerView.
36 public static interface Observer {
38 * Called when the BannerView is dismissed.
39 * @param banner BannerView being dismissed.
41 public void onBannerDismissed(AppBannerView banner);
44 * Called when the button has been clicked.
45 * @param banner BannerView firing the event.
47 public void onButtonClicked(AppBannerView banner);
50 // Maximum number of stars in the rating.
51 private static final int NUM_STARS = 5;
53 // XML layout for the BannerView.
54 private static final int BANNER_LAYOUT = R.layout.app_banner_view;
56 // True if the layout is in left-to-right layout mode (regular mode).
57 private final boolean mIsLayoutLTR;
59 // Class to alert when the BannerView is dismissed.
60 private Observer mObserver;
62 // Views comprising the app banner.
63 private ImageView mIconView;
64 private TextView mTitleView;
65 private Button mButtonView;
66 private ImageView mRatingView;
67 private ImageView mLogoView;
68 private ClipDrawable mRatingClipperDrawable;
70 // Information about the package.
72 private String mPackageName;
73 private float mAppRating;
75 // Variables used during layout calculations and saved to avoid reallocations.
76 private final Point mSpaceMain;
77 private final Point mSpaceForLogo;
78 private final Point mSpaceForRating;
79 private final Point mSpaceForTitle;
82 * Creates a BannerView and adds it to the given ContentView.
83 * @param contentView ContentView to display the BannerView for.
84 * @param observer Class that is alerted for BannerView events.
85 * @param icon Icon to display for the app.
86 * @param title Title of the app.
87 * @param rating Rating of the app.
88 * @param buttonText Text to show on the button.
90 public static AppBannerView create(ContentView contentView, Observer observer, String url,
91 String packageName, String title, Drawable icon, float rating, String buttonText) {
92 Context context = contentView.getContext().getApplicationContext();
93 AppBannerView banner =
94 (AppBannerView) LayoutInflater.from(context).inflate(BANNER_LAYOUT, null);
95 banner.initialize(observer, url, packageName, title, icon, rating, buttonText);
96 banner.addToView(contentView);
101 * Creates a BannerView from an XML layout.
103 public AppBannerView(Context context, AttributeSet attrs) {
104 super(context, attrs);
105 mIsLayoutLTR = !LocalizationUtils.isSystemLayoutDirectionRtl();
106 mSpaceMain = new Point();
107 mSpaceForLogo = new Point();
108 mSpaceForRating = new Point();
109 mSpaceForTitle = new Point();
113 * Initialize the banner with information about the package.
114 * @param observer Class to alert about changes to the banner.
115 * @param title Title of the package.
116 * @param icon Icon for the package.
117 * @param rating Play store rating for the package.
118 * @param buttonText Text to show on the button.
120 private void initialize(Observer observer, String url, String packageName,
121 String title, Drawable icon, float rating, String buttonText) {
122 mObserver = observer;
124 mPackageName = packageName;
126 // Pull out all of the controls we are expecting.
127 mIconView = (ImageView) findViewById(R.id.app_icon);
128 mTitleView = (TextView) findViewById(R.id.app_title);
129 mButtonView = (Button) findViewById(R.id.app_install_button);
130 mRatingView = (ImageView) findViewById(R.id.app_rating);
131 mLogoView = (ImageView) findViewById(R.id.store_logo);
132 assert mIconView != null;
133 assert mTitleView != null;
134 assert mButtonView != null;
135 assert mLogoView != null;
136 assert mRatingView != null;
138 // Set up the button to fire an event.
139 mButtonView.setOnClickListener(this);
141 // Configure the controls with the package information.
142 mTitleView.setText(title);
143 mIconView.setImageDrawable(icon);
145 mButtonView.setText(buttonText);
146 initializeRatingView();
149 private void initializeRatingView() {
150 // Set up the stars Drawable.
151 Drawable ratingDrawable = getResources().getDrawable(R.drawable.app_banner_rating);
152 mRatingClipperDrawable =
153 new ClipDrawable(ratingDrawable, Gravity.START, ClipDrawable.HORIZONTAL);
154 mRatingView.setImageDrawable(mRatingClipperDrawable);
156 // Clips the ImageView for the ratings so that it shows an appropriate number of stars.
157 // Ratings are rounded to the nearest 0.5 increment, like in the Play Store.
158 float roundedRating = Math.round(mAppRating * 2) / 2.0f;
159 float percentageRating = roundedRating / NUM_STARS;
160 int clipLevel = (int) (percentageRating * 10000);
161 mRatingClipperDrawable.setLevel(clipLevel);
165 public void onClick(View view) {
166 if (mObserver != null && view == mButtonView) {
167 mObserver.onButtonClicked(this);
172 * Removes this View from its parent and alerts any observers of the dismissal.
173 * @return Whether or not the View was successfully dismissed.
176 boolean removeFromParent() {
177 boolean removed = super.removeFromParent();
178 if (removed) mObserver.onBannerDismissed(this);
183 * @return The URL that the banner was created for.
190 * @return The package that the banner is displaying information for.
192 String getPackageName() {
197 * Determine how big an icon needs to be for the Layout.
198 * @param context Context to grab resources from.
199 * @return How big the icon is expected to be, in pixels.
201 static int getIconSize(Context context) {
202 return context.getResources().getDimensionPixelSize(R.dimen.app_banner_icon_size);
206 * Fade the banner back into view.
209 protected void onAttachedToWindow() {
210 super.onAttachedToWindow();
211 ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 1.f).setDuration(
212 MS_ANIMATION_DURATION).start();
213 setVisibility(VISIBLE);
217 * Immediately hide the banner to avoid having them show up in snapshots.
220 protected void onDetachedFromWindow() {
221 super.onDetachedFromWindow();
222 setVisibility(INVISIBLE);
226 * Measurement for components of the banner are performed using the following procedure:
228 * 00000000000000000000000000000000000000000000000000000
229 * 01111155555555555555555555555555555555555555555555550
230 * 01111155555555555555555555555555555555555555555555550
231 * 01111144444444444440000000000000000000000222222222220
232 * 01111133333333333330000000000000000000000222222222220
233 * 00000000000000000000000000000000000000000000000000000
235 * 0) A maximum width is enforced on the banner, based on the smallest width of the screen,
236 * then padding defined by the 9-patch background Drawable is subtracted from all sides.
237 * 1) The icon takes up the left side of the banner.
238 * 2) The install button occupies the bottom-right of the banner.
239 * 3) The Google Play logo occupies the space to the left of the button.
240 * 4) The rating is assigned space above the logo and below the title.
241 * 5) The title is assigned whatever space is left. The maximum height of the banner is defined
242 * by deducting the height of either the install button or the logo + rating, (which is
243 * bigger). If the title cannot fit two lines comfortably, it is shrunk down to one.
245 * See {@link #android.view.View.onMeasure(int, int)} for the parameters.
248 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
249 // Enforce a maximum width on the banner.
250 Resources res = getResources();
251 float density = res.getDisplayMetrics().density;
252 int screenSmallestWidth = (int) (res.getConfiguration().smallestScreenWidthDp * density);
253 int definedMaxWidth = (int) res.getDimension(R.dimen.app_banner_max_width);
254 int specWidth = MeasureSpec.getSize(widthMeasureSpec);
255 int maxWidth = Math.min(Math.min(specWidth, definedMaxWidth), screenSmallestWidth);
256 int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
258 // Track how much space is available for the banner content.
259 mSpaceMain.x = maxWidth - ApiCompatibilityUtils.getPaddingStart(this)
260 - ApiCompatibilityUtils.getPaddingEnd(this);
261 mSpaceMain.y = maxHeight - getPaddingTop() - getPaddingBottom();
263 // Measure the icon, which hugs the banner's starting edge and defines the banner's height.
264 measureChildForSpace(mIconView, mSpaceMain);
265 mSpaceMain.x -= getChildWidthWithMargins(mIconView);
266 mSpaceMain.y = getChildHeightWithMargins(mIconView) + getPaddingTop() + getPaddingBottom();
268 // Measure the install button, which sits in the bottom-right corner.
269 measureChildForSpace(mButtonView, mSpaceMain);
271 // Measure the logo, which sits in the bottom-left corner next to the icon.
272 mSpaceForLogo.x = mSpaceMain.x - getChildWidthWithMargins(mButtonView);
273 mSpaceForLogo.y = mSpaceMain.y;
274 measureChildForSpace(mLogoView, mSpaceForLogo);
276 // Measure the star rating, which sits below the title and above the logo.
277 mSpaceForRating.x = mSpaceForLogo.x;
278 mSpaceForRating.y = mSpaceForLogo.y - getChildHeightWithMargins(mLogoView);
279 measureChildForSpace(mRatingView, mSpaceForRating);
281 // The app title spans the top of the banner.
282 mSpaceForTitle.x = mSpaceMain.x;
283 mSpaceForTitle.y = mSpaceMain.y - getChildHeightWithMargins(mLogoView)
284 - getChildHeightWithMargins(mRatingView);
285 mTitleView.setMaxLines(2);
286 measureChildForSpace(mTitleView, mSpaceForTitle);
288 // Ensure the text doesn't get cut in half through one of the lines.
289 int requiredHeight = mTitleView.getLineHeight() * mTitleView.getLineCount();
290 if (getChildHeightWithMargins(mTitleView) < requiredHeight) {
291 mTitleView.setMaxLines(1);
292 measureChildForSpace(mTitleView, mSpaceForTitle);
295 // Set the measured dimensions for the banner.
296 int measuredHeight = mIconView.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();
297 setMeasuredDimension(maxWidth, measuredHeight);
301 * Lays out the controls according to the algorithm in {@link #onMeasure}.
302 * See {@link #android.view.View.onLayout(boolean, int, int, int, int)} for the parameters.
305 protected void onLayout(boolean changed, int l, int t, int r, int b) {
306 int top = getPaddingTop();
307 int bottom = getMeasuredHeight() - getPaddingBottom();
308 int start = ApiCompatibilityUtils.getPaddingStart(this);
309 int end = getMeasuredWidth() - ApiCompatibilityUtils.getPaddingEnd(this);
312 int iconWidth = mIconView.getMeasuredWidth();
313 int iconLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - iconWidth);
314 mIconView.layout(iconLeft, top, iconLeft + iconWidth, top + mIconView.getMeasuredHeight());
315 start += getChildWidthWithMargins(mIconView);
317 // Lay out the app title text. The TextView seems to internally account for its margins.
318 int titleWidth = mTitleView.getMeasuredWidth();
320 int titleBottom = titleTop + getChildHeightWithMargins(mTitleView);
321 int titleLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - titleWidth);
322 mTitleView.layout(titleLeft, titleTop, titleLeft + titleWidth, titleBottom);
323 top += getChildHeightWithMargins(mTitleView);
325 // Lay out the app rating below the title.
326 int starWidth = mRatingView.getMeasuredWidth();
327 int starTop = top + ((MarginLayoutParams) mRatingView.getLayoutParams()).topMargin;
328 int starBottom = starTop + mRatingView.getMeasuredHeight();
329 int starLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - starWidth);
330 mRatingView.layout(starLeft, starTop, starLeft + starWidth, starBottom);
332 // Lay out the logo in the bottom-left.
333 int logoWidth = mLogoView.getMeasuredWidth();
334 int logoBottom = bottom - ((MarginLayoutParams) mLogoView.getLayoutParams()).bottomMargin;
335 int logoTop = logoBottom - mLogoView.getMeasuredHeight();
336 int logoLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - logoWidth);
337 mLogoView.layout(logoLeft, logoTop, logoLeft + logoWidth, logoBottom);
339 // Lay out the install button in the bottom-right corner.
340 int buttonHeight = mButtonView.getMeasuredHeight();
341 int buttonWidth = mButtonView.getMeasuredWidth();
342 int buttonRight = mIsLayoutLTR ? end : (getMeasuredWidth() - end + buttonWidth);
343 int buttonLeft = buttonRight - buttonWidth;
344 mButtonView.layout(buttonLeft, bottom - buttonHeight, buttonRight, bottom);
348 * Calculates how wide the given View has been measured to be, including its margins.
349 * @param child Child to measure.
350 * @return Measured width of the child plus its margins.
352 private int getChildWidthWithMargins(View child) {
353 MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
354 return child.getMeasuredWidth() + ApiCompatibilityUtils.getMarginStart(params)
355 + ApiCompatibilityUtils.getMarginEnd(params);
359 * Calculates how tall the given View has been measured to be, including its margins.
360 * @param child Child to measure.
361 * @return Measured height of the child plus its margins.
363 private static int getChildHeightWithMargins(View child) {
364 MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
365 return child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
369 * Measures a child so that it fits within the given space, taking into account heights defined
371 * @param child View to measure.
372 * @param available Available space, with width stored in the x coordinate and height in the y.
374 private void measureChildForSpace(View child, Point available) {
375 int childHeight = child.getLayoutParams().height;
376 int maxHeight = childHeight > 0 ? Math.min(available.y, childHeight) : available.y;
377 int widthSpec = MeasureSpec.makeMeasureSpec(available.x, MeasureSpec.AT_MOST);
378 int heightSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST);
379 measureChildWithMargins(child, widthSpec, 0, heightSpec, 0);