Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / android / java / src / org / chromium / chrome / browser / banners / AppBannerView.java
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.
4
5 package org.chromium.chrome.browser.banners;
6
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;
20
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;
25
26 /**
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.
31  */
32 public class AppBannerView extends SwipableOverlayView implements View.OnClickListener {
33     /**
34      * Class that is alerted about things happening to the BannerView.
35      */
36     public static interface Observer {
37         /**
38          * Called when the BannerView is dismissed.
39          * @param banner BannerView being dismissed.
40          */
41         public void onBannerDismissed(AppBannerView banner);
42
43         /**
44          * Called when the button has been clicked.
45          * @param banner BannerView firing the event.
46          */
47         public void onButtonClicked(AppBannerView banner);
48     }
49
50     // Maximum number of stars in the rating.
51     private static final int NUM_STARS = 5;
52
53     // XML layout for the BannerView.
54     private static final int BANNER_LAYOUT = R.layout.app_banner_view;
55
56     // True if the layout is in left-to-right layout mode (regular mode).
57     private final boolean mIsLayoutLTR;
58
59     // Class to alert when the BannerView is dismissed.
60     private Observer mObserver;
61
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;
69
70     // Information about the package.
71     private String mUrl;
72     private String mPackageName;
73     private float mAppRating;
74
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;
80
81     /**
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.
89      */
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);
97         return banner;
98     }
99
100     /**
101      * Creates a BannerView from an XML layout.
102      */
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();
110     }
111
112     /**
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.
119      */
120     private void initialize(Observer observer, String url, String packageName,
121             String title, Drawable icon, float rating, String buttonText) {
122         mObserver = observer;
123         mUrl = url;
124         mPackageName = packageName;
125
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;
137
138         // Set up the button to fire an event.
139         mButtonView.setOnClickListener(this);
140
141         // Configure the controls with the package information.
142         mTitleView.setText(title);
143         mIconView.setImageDrawable(icon);
144         mAppRating = rating;
145         mButtonView.setText(buttonText);
146         initializeRatingView();
147     }
148
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);
155
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);
162     }
163
164     @Override
165     public void onClick(View view) {
166         if (mObserver != null && view == mButtonView) {
167             mObserver.onButtonClicked(this);
168         }
169     }
170
171     /**
172      * Removes this View from its parent and alerts any observers of the dismissal.
173      * @return Whether or not the View was successfully dismissed.
174      */
175     @Override
176     boolean removeFromParent() {
177         boolean removed = super.removeFromParent();
178         if (removed) mObserver.onBannerDismissed(this);
179         return removed;
180     }
181
182     /**
183      * @return The URL that the banner was created for.
184      */
185     String getUrl() {
186         return mUrl;
187     }
188
189     /**
190      * @return The package that the banner is displaying information for.
191      */
192     String getPackageName() {
193         return mPackageName;
194     }
195
196     /**
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.
200      */
201     static int getIconSize(Context context) {
202         return context.getResources().getDimensionPixelSize(R.dimen.app_banner_icon_size);
203     }
204
205     /**
206      * Fade the banner back into view.
207      */
208     @Override
209     protected void onAttachedToWindow() {
210         super.onAttachedToWindow();
211         ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 1.f).setDuration(
212                 MS_ANIMATION_DURATION).start();
213         setVisibility(VISIBLE);
214     }
215
216     /**
217      * Immediately hide the banner to avoid having them show up in snapshots.
218      */
219     @Override
220     protected void onDetachedFromWindow() {
221         super.onDetachedFromWindow();
222         setVisibility(INVISIBLE);
223     }
224
225     /**
226      * Measurement for components of the banner are performed using the following procedure:
227      *
228      * 00000000000000000000000000000000000000000000000000000
229      * 01111155555555555555555555555555555555555555555555550
230      * 01111155555555555555555555555555555555555555555555550
231      * 01111144444444444440000000000000000000000222222222220
232      * 01111133333333333330000000000000000000000222222222220
233      * 00000000000000000000000000000000000000000000000000000
234      *
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.
244      *
245      * See {@link #android.view.View.onMeasure(int, int)} for the parameters.
246      */
247     @Override
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);
257
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();
262
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();
267
268         // Measure the install button, which sits in the bottom-right corner.
269         measureChildForSpace(mButtonView, mSpaceMain);
270
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);
275
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);
280
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);
287
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);
293         }
294
295         // Set the measured dimensions for the banner.
296         int measuredHeight = mIconView.getMeasuredHeight() + getPaddingTop() + getPaddingBottom();
297         setMeasuredDimension(maxWidth, measuredHeight);
298     }
299
300     /**
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.
303      */
304     @Override
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);
310
311         // Lay out the icon.
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);
316
317         // Lay out the app title text.  The TextView seems to internally account for its margins.
318         int titleWidth = mTitleView.getMeasuredWidth();
319         int titleTop = top;
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);
324
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);
331
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);
338
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);
345     }
346
347     /**
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.
351      */
352     private int getChildWidthWithMargins(View child) {
353         MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
354         return child.getMeasuredWidth() + ApiCompatibilityUtils.getMarginStart(params)
355                 + ApiCompatibilityUtils.getMarginEnd(params);
356     }
357
358     /**
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.
362      */
363     private static int getChildHeightWithMargins(View child) {
364         MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
365         return child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
366     }
367
368     /**
369      * Measures a child so that it fits within the given space, taking into account heights defined
370      * in the layout.
371      * @param child     View to measure.
372      * @param available Available space, with width stored in the x coordinate and height in the y.
373      */
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);
380     }
381 }