| // Copyright 2016 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.payments.ui; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Color; |
| import android.graphics.Typeface; |
| import android.text.SpannableStringBuilder; |
| import android.text.TextUtils; |
| import android.text.TextUtils.TruncateAt; |
| import android.text.style.StyleSpan; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.Button; |
| import android.widget.GridLayout; |
| import android.widget.ImageButton; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.RadioButton; |
| import android.widget.TextView; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.widget.DualControlLayout; |
| import org.chromium.chrome.browser.widget.TintedDrawable; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import javax.annotation.Nullable; |
| |
| /** |
| * Represents a single section in the {@link PaymentRequestUI} that flips between multiple states. |
| * |
| * The row is broken up into three major, vertically-centered sections: |
| * ............................................................................................. |
| * . TITLE | | CHEVRON . |
| * .................................................................| | or . |
| * . LEFT SUMMARY TEXT | RIGHT SUMMARY TEXT | LOGO | ADD . |
| * .................................................................| | or . |
| * . MAIN SECTION CONTENT | | SELECT . |
| * ............................................................................................. |
| * |
| * 1) MAIN CONTENT |
| * The main content is on the left side of the UI. This includes the title of the section and |
| * two bits of optional summary text. Subclasses may extend this class to append more controls |
| * via the {@link #createMainSectionContent} function. |
| * |
| * 2) LOGO |
| * Displays an optional logo (e.g. a credit card image) that floats to the right of the main |
| * content. |
| * |
| * 3) CHEVRON or ADD or SELECT |
| * Drawn to indicate that the current section may be expanded. Displayed only when the view is |
| * in the {@link #DISPLAY_MODE_EXPANDABLE} state and only if an ADD or SELECT button isn't shown. |
| * |
| * There are three states that the UI may flip between; see {@link #DISPLAY_MODE_NORMAL}, |
| * {@link #DISPLAY_MODE_EXPANDABLE}, and {@link #DISPLAY_MODE_FOCUSED} for details. |
| */ |
| public abstract class PaymentRequestSection extends LinearLayout implements View.OnClickListener { |
| public static final String TAG = "PaymentRequestUI"; |
| |
| /** Handles clicks on the widgets and providing data to the PaymentsRequestSection. */ |
| public interface SectionDelegate extends View.OnClickListener { |
| /** |
| * Called when the user selects a radio button option from an {@link OptionSection}. |
| * |
| * @param section Section that was changed. |
| * @param option {@link PaymentOption} that was selected. |
| */ |
| void onPaymentOptionChanged(PaymentRequestSection section, PaymentOption option); |
| |
| /** Called when the user requests adding a new PaymentOption to a given section. */ |
| void onAddPaymentOption(PaymentRequestSection section); |
| |
| /** Checks whether or not the text should be formatted with a bold label. */ |
| boolean isBoldLabelNeeded(PaymentRequestSection section); |
| |
| /** Checks whether or not the user should be allowed to click on controls. */ |
| boolean isAcceptingUserInput(); |
| |
| /** Returns any additional text that needs to be displayed. */ |
| @Nullable String getAdditionalText(PaymentRequestSection section); |
| |
| /** Returns true if the additional text should be stylized as a warning instead of info. */ |
| boolean isAdditionalTextDisplayingWarning(PaymentRequestSection section); |
| |
| /** Called when a section has been clicked. */ |
| void onSectionClicked(PaymentRequestSection section); |
| } |
| |
| /** Edit button mode: Hide the button. */ |
| public static final int EDIT_BUTTON_GONE = 0; |
| |
| /** Edit button mode: Indicate that the section requires a selection. */ |
| public static final int EDIT_BUTTON_SELECT = 1; |
| |
| /** Edit button mode: Indicate that the section requires adding an option. */ |
| public static final int EDIT_BUTTON_ADD = 2; |
| |
| /** Normal mode: White background, displays the item assuming the user accepts it as is. */ |
| static final int DISPLAY_MODE_NORMAL = 3; |
| |
| /** Editable mode: White background, displays the item with an edit chevron. */ |
| static final int DISPLAY_MODE_EXPANDABLE = 4; |
| |
| /** Focused mode: Gray background, more padding, no edit chevron. */ |
| static final int DISPLAY_MODE_FOCUSED = 5; |
| |
| /** Checking mode: Gray background, spinner overlay hides everything except the title. */ |
| static final int DISPLAY_MODE_CHECKING = 6; |
| |
| protected final SectionDelegate mDelegate; |
| protected final int mLargeSpacing; |
| protected final Button mEditButtonView; |
| protected final boolean mIsLayoutInitialized; |
| |
| protected int mDisplayMode = DISPLAY_MODE_NORMAL; |
| |
| private final int mVerticalSpacing; |
| private final int mFocusedBackgroundColor; |
| private final LinearLayout mMainSection; |
| private final ImageView mLogoView; |
| private final ImageView mChevronView; |
| |
| private TextView mTitleView; |
| private LinearLayout mSummaryLayout; |
| private TextView mSummaryLeftTextView; |
| private TextView mSummaryRightTextView; |
| |
| private int mLogoResourceId; |
| private boolean mIsSummaryAllowed = true; |
| |
| /** |
| * Constructs a PaymentRequestSection. |
| * |
| * @param context Context to pull resources from. |
| * @param sectionName Title of the section to display. |
| * @param delegate Delegate to alert when something changes in the dialog. |
| */ |
| private PaymentRequestSection(Context context, String sectionName, SectionDelegate delegate) { |
| super(context); |
| mDelegate = delegate; |
| setOnClickListener(delegate); |
| setOrientation(HORIZONTAL); |
| setGravity(Gravity.CENTER_VERTICAL); |
| |
| // Set the styling of the view. |
| mFocusedBackgroundColor = ApiCompatibilityUtils.getColor( |
| getResources(), R.color.payments_section_edit_background); |
| mLargeSpacing = |
| getResources().getDimensionPixelSize(R.dimen.payments_section_large_spacing); |
| mVerticalSpacing = |
| getResources().getDimensionPixelSize(R.dimen.payments_section_vertical_spacing); |
| setPadding(mLargeSpacing, mVerticalSpacing, mLargeSpacing, mVerticalSpacing); |
| |
| // Create the main content. |
| mMainSection = prepareMainSection(sectionName); |
| mLogoView = isLogoNecessary() ? createAndAddLogoView(this, 0, mLargeSpacing) : null; |
| mEditButtonView = createAndAddEditButton(this); |
| mChevronView = createAndAddChevron(this); |
| mIsLayoutInitialized = true; |
| setDisplayMode(DISPLAY_MODE_NORMAL); |
| } |
| |
| /** |
| * Sets what logo should be displayed. |
| * |
| * @param resourceId ID of the logo to display. |
| */ |
| protected void setLogoResource(int resourceId) { |
| assert isLogoNecessary(); |
| mLogoResourceId = resourceId; |
| mLogoView.setImageResource(resourceId); |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent event) { |
| // Allow touches to propagate to children only if the layout can be interacted with. |
| return !mDelegate.isAcceptingUserInput(); |
| } |
| |
| @Override |
| public final void onClick(View v) { |
| if (!mDelegate.isAcceptingUserInput()) return; |
| |
| // Handle clicking on "ADD" or "SELECT". |
| if (v == mEditButtonView) { |
| if (getEditButtonState() == EDIT_BUTTON_ADD) { |
| mDelegate.onAddPaymentOption(this); |
| } else { |
| mDelegate.onSectionClicked(this); |
| } |
| return; |
| } |
| |
| handleClick(v); |
| updateControlLayout(); |
| } |
| |
| /** Handles clicks on the PaymentRequestSection. */ |
| protected void handleClick(View v) { } |
| |
| /** |
| * Called when the UI is telling the section that it has either gained or lost focus. |
| */ |
| public void focusSection(boolean shouldFocus) { |
| setDisplayMode(shouldFocus ? DISPLAY_MODE_FOCUSED : DISPLAY_MODE_EXPANDABLE); |
| } |
| |
| /** |
| * Updates what Views are displayed and how they look. |
| * |
| * @param displayMode What mode the widget is being displayed in. |
| */ |
| public void setDisplayMode(int displayMode) { |
| mDisplayMode = displayMode; |
| updateControlLayout(); |
| } |
| |
| /** |
| * Changes what is being displayed in the summary. |
| * |
| * @param leftText Text to display on the left side. If null, the whole row hides. |
| * @param rightText Text to display on the right side. If null, only the right View hides. |
| */ |
| public void setSummaryText( |
| @Nullable CharSequence leftText, @Nullable CharSequence rightText) { |
| mSummaryLeftTextView.setText(leftText); |
| mSummaryRightTextView.setText(rightText); |
| mSummaryRightTextView.setVisibility(TextUtils.isEmpty(rightText) ? GONE : VISIBLE); |
| updateControlLayout(); |
| } |
| |
| /** |
| * Sets how the summary text should be displayed. |
| * |
| * @param leftTruncate How to truncate the left summary text. Set to null to clear. |
| * @param rightTruncate How to truncate the right summary text. Set to null to clear. |
| */ |
| public void setSummaryProperties(@Nullable TruncateAt leftTruncate, boolean leftIsSingleLine, |
| @Nullable TruncateAt rightTruncate, boolean rightIsSingleLine) { |
| mSummaryLeftTextView.setEllipsize(leftTruncate); |
| mSummaryLeftTextView.setSingleLine(leftIsSingleLine); |
| |
| mSummaryRightTextView.setEllipsize(rightTruncate); |
| mSummaryRightTextView.setSingleLine(rightIsSingleLine); |
| } |
| |
| /** |
| * Subclasses may override this method to add additional controls to the layout. |
| * |
| * @param mainSectionLayout Layout containing all of the main content of the section. |
| */ |
| protected abstract void createMainSectionContent(LinearLayout mainSectionLayout); |
| |
| /** |
| * Sets whether the edit button may be interacted with. |
| * |
| * @param isEnabled Whether the button may be interacted with. |
| */ |
| public void setIsEditButtonEnabled(boolean isEnabled) { |
| mEditButtonView.setEnabled(isEnabled); |
| } |
| |
| /** |
| * Sets whether the summary text can be displayed. |
| * |
| * @param isAllowed Whether to display the summary text when needed. |
| */ |
| protected void setIsSummaryAllowed(boolean isAllowed) { |
| mIsSummaryAllowed = isAllowed; |
| } |
| |
| /** @return Whether or not the logo should be displayed. */ |
| protected boolean isLogoNecessary() { |
| return false; |
| } |
| |
| /** |
| * Returns the state of the edit button, which is hidden by default. |
| * |
| * @return State of the edit button. |
| */ |
| public int getEditButtonState() { |
| return EDIT_BUTTON_GONE; |
| } |
| |
| /** |
| * Creates the main section. Subclasses must call super#createMainSection() immediately to |
| * guarantee that Views are added in the correct order. |
| * |
| * @param sectionName Title to display for the section. |
| */ |
| private LinearLayout prepareMainSection(String sectionName) { |
| // The main section is a vertical linear layout that subclasses can append to. |
| LinearLayout mainSectionLayout = new LinearLayout(getContext()); |
| mainSectionLayout.setOrientation(VERTICAL); |
| LinearLayout.LayoutParams mainParams = new LayoutParams(0, LayoutParams.WRAP_CONTENT); |
| mainParams.weight = 1; |
| addView(mainSectionLayout, mainParams); |
| |
| // The title is always displayed for the row at the top of the main section. |
| mTitleView = new TextView(getContext()); |
| mTitleView.setText(sectionName); |
| ApiCompatibilityUtils.setTextAppearance( |
| mTitleView, R.style.PaymentsUiSectionHeader); |
| mainSectionLayout.addView( |
| mTitleView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); |
| |
| // Create the two TextViews for showing the summary text. |
| mSummaryLeftTextView = new TextView(getContext()); |
| mSummaryLeftTextView.setId(R.id.payments_left_summary_label); |
| ApiCompatibilityUtils.setTextAppearance( |
| mSummaryLeftTextView, R.style.PaymentsUiSectionDefaultText); |
| |
| mSummaryRightTextView = new TextView(getContext()); |
| ApiCompatibilityUtils.setTextAppearance( |
| mSummaryRightTextView, R.style.PaymentsUiSectionDefaultText); |
| ApiCompatibilityUtils.setTextAlignment(mSummaryRightTextView, TEXT_ALIGNMENT_TEXT_END); |
| |
| // The main TextView sucks up all the available space. |
| LinearLayout.LayoutParams leftLayoutParams = new LinearLayout.LayoutParams( |
| 0, LayoutParams.WRAP_CONTENT); |
| leftLayoutParams.weight = 1; |
| |
| LinearLayout.LayoutParams rightLayoutParams = new LinearLayout.LayoutParams( |
| LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| ApiCompatibilityUtils.setMarginStart( |
| rightLayoutParams, |
| getContext().getResources().getDimensionPixelSize( |
| R.dimen.payments_section_small_spacing)); |
| |
| // The summary section displays up to two TextViews side by side. |
| mSummaryLayout = new LinearLayout(getContext()); |
| mSummaryLayout.addView(mSummaryLeftTextView, leftLayoutParams); |
| mSummaryLayout.addView(mSummaryRightTextView, rightLayoutParams); |
| mainSectionLayout.addView(mSummaryLayout, new LinearLayout.LayoutParams( |
| LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); |
| setSummaryText(null, null); |
| |
| createMainSectionContent(mainSectionLayout); |
| return mainSectionLayout; |
| } |
| |
| private static ImageView createAndAddLogoView( |
| ViewGroup parent, int resourceId, int startMargin) { |
| ImageView view = new ImageView(parent.getContext()); |
| view.setBackgroundResource(R.drawable.payments_ui_logo_bg); |
| if (resourceId != 0) view.setImageResource(resourceId); |
| |
| // The logo has a pre-defined height and width. |
| LayoutParams params = new LayoutParams( |
| parent.getResources().getDimensionPixelSize(R.dimen.payments_section_logo_width), |
| parent.getResources().getDimensionPixelSize(R.dimen.payments_section_logo_height)); |
| ApiCompatibilityUtils.setMarginStart(params, startMargin); |
| parent.addView(view, params); |
| return view; |
| } |
| |
| private Button createAndAddEditButton(ViewGroup parent) { |
| Resources resources = parent.getResources(); |
| Button view = DualControlLayout.createButtonForLayout( |
| parent.getContext(), true, resources.getString(R.string.select), this); |
| view.setId(R.id.payments_section); |
| |
| LayoutParams params = |
| new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| ApiCompatibilityUtils.setMarginStart(params, mLargeSpacing); |
| parent.addView(view, params); |
| return view; |
| } |
| |
| private ImageView createAndAddChevron(ViewGroup parent) { |
| Resources resources = parent.getResources(); |
| TintedDrawable chevron = TintedDrawable.constructTintedDrawable( |
| resources, R.drawable.ic_expanded, R.color.payments_section_chevron); |
| |
| ImageView view = new ImageView(parent.getContext()); |
| view.setImageDrawable(chevron); |
| |
| // Wrap whatever image is passed in. |
| LayoutParams params = |
| new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| ApiCompatibilityUtils.setMarginStart(params, mLargeSpacing); |
| parent.addView(view, params); |
| return view; |
| } |
| |
| /** |
| * Called when the section's controls need to be updated after configuration changes. |
| * |
| * Because of the complicated special casing of what controls hide other controls, all calls to |
| * update just one of the controls causes the visibility logic to trigger for all of them. |
| * |
| * Subclasses should call the super method after they update their own controls. |
| */ |
| protected void updateControlLayout() { |
| if (!mIsLayoutInitialized) return; |
| |
| boolean isExpanded = |
| mDisplayMode == DISPLAY_MODE_FOCUSED || mDisplayMode == DISPLAY_MODE_CHECKING; |
| setBackgroundColor(isExpanded ? mFocusedBackgroundColor : Color.WHITE); |
| |
| // Update whether the logo is displayed. |
| if (mLogoView != null) { |
| boolean show = mLogoResourceId != 0 && mDisplayMode != DISPLAY_MODE_FOCUSED; |
| mLogoView.setVisibility(show ? VISIBLE : GONE); |
| } |
| |
| // The button takes precedence over the summary text and the chevron. |
| int editButtonState = getEditButtonState(); |
| if (editButtonState == EDIT_BUTTON_GONE) { |
| mEditButtonView.setVisibility(GONE); |
| mChevronView.setVisibility( |
| mDisplayMode == DISPLAY_MODE_EXPANDABLE ? VISIBLE : GONE); |
| |
| // Update whether the summary is displayed. |
| boolean showSummary = |
| mIsSummaryAllowed && !TextUtils.isEmpty(mSummaryLeftTextView.getText()); |
| mSummaryLayout.setVisibility(showSummary ? VISIBLE : GONE); |
| } else { |
| // Show the edit button and hide the chevron and the summary. |
| boolean isButtonAllowed = mDisplayMode == DISPLAY_MODE_EXPANDABLE |
| || mDisplayMode == DISPLAY_MODE_NORMAL; |
| mSummaryLayout.setVisibility(GONE); |
| mChevronView.setVisibility(GONE); |
| mEditButtonView.setVisibility(isButtonAllowed ? VISIBLE : GONE); |
| mEditButtonView.setText( |
| editButtonState == EDIT_BUTTON_SELECT ? R.string.select : R.string.add); |
| } |
| |
| // The title gains extra spacing when there is another visible view in the main section. |
| int numVisibleMainViews = 0; |
| for (int i = 0; i < mMainSection.getChildCount(); i++) { |
| if (mMainSection.getChildAt(i).getVisibility() == VISIBLE) numVisibleMainViews += 1; |
| } |
| |
| boolean isTitleMarginNecessary = numVisibleMainViews > 1 && isExpanded; |
| int oldMargin = |
| ((ViewGroup.MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin; |
| int newMargin = isTitleMarginNecessary ? mVerticalSpacing : 0; |
| |
| if (oldMargin != newMargin) { |
| ((ViewGroup.MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin = |
| newMargin; |
| requestLayout(); |
| } |
| } |
| |
| /** |
| * Section with three extra TextViews beneath the summary to show additional details. |
| * |
| * ............................................................................ |
| * . TITLE | . |
| * .................................................................| . |
| * . LEFT SUMMARY TEXT | RIGHT SUMMARY TEXT | CHEVRON . |
| * .................................................................| or . |
| * . EXTRA TEXT ONE | ADD . |
| * .................................................................| or . |
| * . EXTRA TEXT TWO | SELECT . |
| * .................................................................| . |
| * . EXTRA TEXT THREE | . |
| * ............................................................................ |
| */ |
| public static class ExtraTextsSection extends PaymentRequestSection { |
| private TextView[] mExtraTextViews; |
| private int mEditButtonState = EDIT_BUTTON_GONE; |
| |
| public ExtraTextsSection(Context context, String sectionName, SectionDelegate delegate) { |
| super(context, sectionName, delegate); |
| setExtraTexts(new String[] {null, null, null}); |
| } |
| |
| @Override |
| protected void createMainSectionContent(LinearLayout mainSectionLayout) { |
| Context context = mainSectionLayout.getContext(); |
| |
| mExtraTextViews = new TextView[3]; |
| for (int i = 0; i < mExtraTextViews.length; i++) { |
| mExtraTextViews[i] = new TextView(context); |
| ApiCompatibilityUtils.setTextAppearance( |
| mExtraTextViews[i], R.style.PaymentsUiSectionDefaultText); |
| mainSectionLayout.addView( |
| mExtraTextViews[i], new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, |
| LayoutParams.WRAP_CONTENT)); |
| } |
| } |
| |
| /** |
| * Sets the CharSequences that are displayed in the extra TextViews. |
| * |
| * @param extraTexts Texts to display in the extra TextViews. |
| */ |
| public void setExtraTexts(CharSequence[] extraTexts) { |
| assert extraTexts.length == mExtraTextViews.length; |
| |
| for (int i = 0; i < mExtraTextViews.length; i++) { |
| mExtraTextViews[i].setText(extraTexts[i]); |
| mExtraTextViews[i].setVisibility(TextUtils.isEmpty(extraTexts[i]) ? GONE : VISIBLE); |
| } |
| } |
| |
| /** |
| * Sets how the extra texts should be displayed. |
| * |
| * @param textsTruncate How to truncate the extra texts. Set the element to null to clear. |
| * @param textsAreSingleLine Whether the extra texts should be displayed in a single line. |
| */ |
| public void setExtraTextsProperties( |
| TruncateAt[] textsTruncate, boolean[] textsAreSingleLine) { |
| assert textsTruncate.length == mExtraTextViews.length; |
| assert textsAreSingleLine.length == mExtraTextViews.length; |
| |
| for (int i = 0; i < mExtraTextViews.length; i++) { |
| mExtraTextViews[i].setEllipsize(textsTruncate[i]); |
| mExtraTextViews[i].setSingleLine(textsAreSingleLine[i]); |
| } |
| } |
| |
| /** Sets the state of the edit button. */ |
| public void setEditButtonState(int state) { |
| mEditButtonState = state; |
| updateControlLayout(); |
| } |
| |
| @Override |
| public int getEditButtonState() { |
| return mEditButtonState; |
| } |
| } |
| |
| /** |
| * Section with an additional Layout for showing a total and how it is broken down. |
| * |
| * Normal mode: Just the summary is displayed. |
| * If no option is selected, the "empty label" is displayed in its place. |
| * Expandable mode: Same as Normal, but shows the chevron. |
| * Focused mode: Hides the summary and chevron, then displays the full set of options. |
| * |
| * ............................................................................ |
| * . TITLE | . |
| * .................................................................| CHERVON . |
| * . LEFT SUMMARY TEXT | RIGHT SUMMARY TEXT | or . |
| * .................................................................| ADD . |
| * . | Line item 1 | $13.99 | or . |
| * . | Line item 2 | $.99 | SELECT . |
| * . | Line item 3 | $2.99 | . |
| * ............................................................................ |
| */ |
| public static class LineItemBreakdownSection extends PaymentRequestSection { |
| private GridLayout mBreakdownLayout; |
| |
| public LineItemBreakdownSection( |
| Context context, String sectionName, SectionDelegate delegate) { |
| super(context, sectionName, delegate); |
| } |
| |
| @Override |
| protected void createMainSectionContent(LinearLayout mainSectionLayout) { |
| Context context = mainSectionLayout.getContext(); |
| |
| // The breakdown is represented by an end-aligned GridLayout that takes up only as much |
| // space as it needs. The GridLayout ensures a consistent margin between the columns. |
| mBreakdownLayout = new GridLayout(context); |
| mBreakdownLayout.setColumnCount(2); |
| LayoutParams breakdownParams = |
| new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); |
| breakdownParams.gravity = Gravity.END; |
| mainSectionLayout.addView(mBreakdownLayout, breakdownParams); |
| } |
| |
| /** |
| * Updates the total and how it's broken down. |
| * |
| * @param cart The shopping cart contents and the total. |
| */ |
| public void update(ShoppingCart cart) { |
| Context context = mBreakdownLayout.getContext(); |
| |
| // Update the summary to display information about the total. |
| setSummaryText(cart.getTotal().getLabel(), createValueString( |
| cart.getTotal().getCurrency(), cart.getTotal().getPrice(), true)); |
| |
| mBreakdownLayout.removeAllViews(); |
| if (cart.getContents() == null) return; |
| |
| int maximumDescriptionWidthPx = |
| ((View) mBreakdownLayout.getParent()).getWidth() * 2 / 3; |
| |
| // Update the breakdown, using one row per {@link LineItem}. |
| int numItems = cart.getContents().size(); |
| mBreakdownLayout.setRowCount(numItems); |
| for (int i = 0; i < numItems; i++) { |
| LineItem item = cart.getContents().get(i); |
| |
| TextView description = new TextView(context); |
| ApiCompatibilityUtils.setTextAppearance( |
| description, R.style.PaymentsUiSectionDescriptiveTextEndAligned); |
| description.setText(item.getLabel()); |
| description.setEllipsize(TruncateAt.END); |
| description.setMaxLines(2); |
| if (maximumDescriptionWidthPx > 0) { |
| description.setMaxWidth(maximumDescriptionWidthPx); |
| } |
| |
| TextView amount = new TextView(context); |
| ApiCompatibilityUtils.setTextAppearance( |
| amount, R.style.PaymentsUiSectionDescriptiveTextEndAligned); |
| amount.setText(createValueString(item.getCurrency(), item.getPrice(), false)); |
| |
| // Each item is represented by a row in the GridLayout. |
| GridLayout.LayoutParams descriptionParams = new GridLayout.LayoutParams( |
| GridLayout.spec(i, 1, GridLayout.END), |
| GridLayout.spec(0, 1, GridLayout.END)); |
| GridLayout.LayoutParams amountParams = new GridLayout.LayoutParams( |
| GridLayout.spec(i, 1, GridLayout.END), |
| GridLayout.spec(1, 1, GridLayout.END)); |
| ApiCompatibilityUtils.setMarginStart(amountParams, |
| context.getResources().getDimensionPixelSize( |
| R.dimen.payments_section_descriptive_item_spacing)); |
| |
| mBreakdownLayout.addView(description, descriptionParams); |
| mBreakdownLayout.addView(amount, amountParams); |
| } |
| } |
| |
| /** |
| * Builds a CharSequence that displays a value in a particular currency. |
| * |
| * @param currency Currency of the value being displayed. |
| * @param value Value to display. |
| * @param isValueBold Whether or not to bold the item. |
| * @return CharSequence that represents the whole value. |
| */ |
| private CharSequence createValueString(String currency, String value, boolean isValueBold) { |
| SpannableStringBuilder valueBuilder = new SpannableStringBuilder(); |
| valueBuilder.append(currency); |
| valueBuilder.append(" "); |
| |
| int boldStartIndex = valueBuilder.length(); |
| valueBuilder.append(value); |
| |
| if (isValueBold) { |
| valueBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), boldStartIndex, |
| boldStartIndex + value.length(), 0); |
| } |
| |
| return valueBuilder; |
| } |
| |
| @Override |
| protected void updateControlLayout() { |
| if (!mIsLayoutInitialized) return; |
| |
| mBreakdownLayout.setVisibility(mDisplayMode == DISPLAY_MODE_FOCUSED ? VISIBLE : GONE); |
| super.updateControlLayout(); |
| } |
| } |
| |
| /** |
| * Section that allows selecting one thing from a set of mutually-exclusive options. |
| * |
| * Normal mode: The summary text displays the selected option, and the icon for the option |
| * is displayed in the logo section (if it exists). |
| * If no option is selected, the "empty label" is displayed in its place. |
| * This is important for shipping options (e.g.) because there will be no |
| * option selected by default and a prompt can be displayed. |
| * Expandable mode: Same as Normal, but shows the chevron. |
| * Focused mode: Hides the summary and chevron, then displays the full set of options. |
| * |
| * ............................................................................................. |
| * . TITLE | | . |
| * .................................................................| | . |
| * . LEFT SUMMARY TEXT | RIGHT SUMMARY TEXT | | . |
| * .................................................................| | CHEVRON . |
| * . Descriptive text that spans all three columns because it can. | | or . |
| * . ! Warning text that displays a big scary warning and icon. | LOGO | ADD . |
| * . O Option 1 ICON 1 | | or . |
| * . O Option 2 ICON 2 | | SELECT . |
| * . O Option 3 ICON 3 | | . |
| * . + ADD THING | | . |
| * ............................................................................................. |
| */ |
| public static class OptionSection extends PaymentRequestSection { |
| |
| private static final int INVALID_OPTION_INDEX = -1; |
| |
| private final List<TextView> mLabelsForTest = new ArrayList<>(); |
| private boolean mCanAddItems = true; |
| |
| /** |
| * Displays a row representing either a selectable option or some flavor text. |
| * |
| * + The "button" is on the left and shows either an icon or a radio button to represent th |
| * row type. |
| * + The "label" is text describing the row. |
| * + The "icon" is a logo representing the option, like a credit card. |
| */ |
| public class OptionRow { |
| private static final int OPTION_ROW_TYPE_OPTION = 0; |
| private static final int OPTION_ROW_TYPE_ADD = 1; |
| private static final int OPTION_ROW_TYPE_DESCRIPTION = 2; |
| private static final int OPTION_ROW_TYPE_WARNING = 3; |
| |
| private final int mRowType; |
| private final PaymentOption mOption; |
| private final View mButton; |
| private final TextView mLabel; |
| private final View mIcon; |
| |
| public OptionRow(GridLayout parent, int rowIndex, int rowType, PaymentOption item, |
| boolean isSelected) { |
| boolean iconExists = item != null && item.getDrawableIconId() != 0; |
| boolean isEnabled = item != null && item.isValid(); |
| mRowType = rowType; |
| mOption = item; |
| mButton = createButton(parent, rowIndex, isSelected, isEnabled); |
| mLabel = createLabel(parent, rowIndex, iconExists, isEnabled); |
| mIcon = iconExists ? createIcon(parent, rowIndex) : null; |
| } |
| |
| /** Sets the selected state of this item, alerting the delegate if selected. */ |
| public void setChecked(boolean isChecked) { |
| if (mOption == null) return; |
| |
| ((RadioButton) mButton).setChecked(isChecked); |
| if (isChecked) { |
| updateSelectedItem(mOption); |
| mDelegate.onPaymentOptionChanged(OptionSection.this, mOption); |
| } |
| } |
| |
| /** Change the label for the row. */ |
| public void setLabel(int stringId) { |
| setLabel(getContext().getString(stringId)); |
| } |
| |
| /** Change the label for the row. */ |
| public void setLabel(CharSequence string) { |
| mLabel.setText(string); |
| } |
| |
| /** Set the button identifier for the option. */ |
| public void setButtonId(int id) { |
| mButton.setId(id); |
| } |
| |
| /** @return the label for the row. */ |
| @VisibleForTesting |
| public CharSequence getLabelText() { |
| return mLabel.getText(); |
| } |
| |
| private View createButton( |
| GridLayout parent, int rowIndex, boolean isSelected, boolean isEnabled) { |
| if (mRowType == OPTION_ROW_TYPE_DESCRIPTION) return null; |
| |
| Context context = parent.getContext(); |
| View view; |
| |
| if (mRowType == OPTION_ROW_TYPE_OPTION) { |
| // Show a radio button indicating whether the PaymentOption is selected. |
| RadioButton button = new RadioButton(context); |
| button.setChecked(isSelected && isEnabled); |
| button.setEnabled(isEnabled); |
| view = button; |
| } else { |
| // Show an icon representing the row type, defaulting to the add button. |
| int drawableId; |
| int drawableTint; |
| if (mRowType == OPTION_ROW_TYPE_WARNING) { |
| drawableId = R.drawable.ic_warning_white_24dp; |
| drawableTint = R.color.error_text_color; |
| } else { |
| drawableId = R.drawable.plus; |
| drawableTint = R.color.light_active_color; |
| } |
| |
| TintedDrawable tintedDrawable = TintedDrawable.constructTintedDrawable( |
| context.getResources(), drawableId, drawableTint); |
| ImageButton button = new ImageButton(context); |
| button.setBackground(null); |
| button.setImageDrawable(tintedDrawable); |
| button.setPadding(0, 0, 0, 0); |
| view = button; |
| } |
| |
| // The button hugs left. |
| GridLayout.LayoutParams buttonParams = new GridLayout.LayoutParams( |
| GridLayout.spec(rowIndex, 1, GridLayout.CENTER), |
| GridLayout.spec(0, 1, GridLayout.CENTER)); |
| buttonParams.topMargin = mVerticalMargin; |
| ApiCompatibilityUtils.setMarginEnd(buttonParams, mLargeSpacing); |
| parent.addView(view, buttonParams); |
| |
| view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); |
| view.setOnClickListener(OptionSection.this); |
| return view; |
| } |
| |
| private TextView createLabel( |
| GridLayout parent, int rowIndex, boolean iconExists, boolean isEnabled) { |
| Context context = parent.getContext(); |
| Resources resources = context.getResources(); |
| |
| // By default, the label appears to the right of the "button" in the second column. |
| // + If there is no button and no icon, the label spans the whole row. |
| // + If there is no icon, the label spans two columns. |
| // + Otherwise, the label occupies only its own column. |
| int columnStart = 1; |
| int columnSpan = iconExists ? 1 : 2; |
| |
| TextView labelView = new TextView(context); |
| if (mRowType == OPTION_ROW_TYPE_OPTION) { |
| // Show the string representing the PaymentOption. |
| ApiCompatibilityUtils.setTextAppearance(labelView, isEnabled |
| ? R.style.PaymentsUiSectionDefaultText |
| : R.style.PaymentsUiSectionDisabledText); |
| labelView.setText(convertOptionToString( |
| mOption, mDelegate.isBoldLabelNeeded(OptionSection.this))); |
| labelView.setEnabled(isEnabled); |
| } else if (mRowType == OPTION_ROW_TYPE_ADD) { |
| // Shows string saying that the user can add a new option, e.g. credit card no. |
| String typeface = resources.getString(R.string.roboto_medium_typeface); |
| int textStyle = resources.getInteger(R.integer.roboto_medium_textstyle); |
| int buttonHeight = resources.getDimensionPixelSize( |
| R.dimen.payments_section_add_button_height); |
| |
| ApiCompatibilityUtils.setTextAppearance( |
| labelView, R.style.PaymentsUiSectionAddButtonLabel); |
| labelView.setMinimumHeight(buttonHeight); |
| labelView.setGravity(Gravity.CENTER_VERTICAL); |
| labelView.setTypeface(Typeface.create(typeface, textStyle)); |
| } else if (mRowType == OPTION_ROW_TYPE_DESCRIPTION) { |
| // The description spans all the columns. |
| columnStart = 0; |
| columnSpan = 3; |
| |
| ApiCompatibilityUtils.setTextAppearance( |
| labelView, R.style.PaymentsUiSectionDescriptiveText); |
| } else if (mRowType == OPTION_ROW_TYPE_WARNING) { |
| // Warnings use two columns. |
| columnSpan = 2; |
| ApiCompatibilityUtils.setTextAppearance( |
| labelView, R.style.PaymentsUiSectionWarningText); |
| } |
| |
| // The label spans two columns if no icon exists. Setting the view width to 0 |
| // forces it to stretch. |
| GridLayout.LayoutParams labelParams = new GridLayout.LayoutParams( |
| GridLayout.spec(rowIndex, 1, GridLayout.CENTER), |
| GridLayout.spec(columnStart, columnSpan, GridLayout.FILL)); |
| labelParams.topMargin = mVerticalMargin; |
| labelParams.width = 0; |
| parent.addView(labelView, labelParams); |
| |
| labelView.setOnClickListener(OptionSection.this); |
| return labelView; |
| } |
| |
| private View createIcon(GridLayout parent, int rowIndex) { |
| // The icon has a pre-defined width. |
| ImageView icon = new ImageView(parent.getContext()); |
| icon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); |
| icon.setBackgroundResource(R.drawable.payments_ui_logo_bg); |
| icon.setImageResource(mOption.getDrawableIconId()); |
| icon.setMaxWidth(mIconMaxWidth); |
| |
| // The icon floats to the right of everything. |
| GridLayout.LayoutParams iconParams = new GridLayout.LayoutParams( |
| GridLayout.spec(rowIndex, 1, GridLayout.CENTER), GridLayout.spec(2, 1)); |
| iconParams.topMargin = mVerticalMargin; |
| ApiCompatibilityUtils.setMarginStart(iconParams, mLargeSpacing); |
| parent.addView(icon, iconParams); |
| |
| icon.setOnClickListener(OptionSection.this); |
| return icon; |
| } |
| } |
| |
| /** Top and bottom margins for each item. */ |
| private final int mVerticalMargin; |
| |
| /** All the possible PaymentOptions in Layout form, then one row for adding new options. */ |
| private final ArrayList<OptionRow> mOptionRows = new ArrayList<>(); |
| |
| /** Width that the icon takes. */ |
| private final int mIconMaxWidth; |
| |
| /** Layout containing all the {@link OptionRow}s. */ |
| private GridLayout mOptionLayout; |
| |
| /** A spinner to show when the user selection is being checked. */ |
| private View mCheckingProgress; |
| |
| /** SectionInformation that is used to populate the views in this section. */ |
| private SectionInformation mSectionInformation; |
| |
| /** |
| * Constructs an OptionSection. |
| * |
| * @param context Context to pull resources from. |
| * @param sectionName Title of the section to display. |
| * @param delegate Delegate to alert when something changes in the dialog. |
| */ |
| public OptionSection(Context context, String sectionName, SectionDelegate delegate) { |
| super(context, sectionName, delegate); |
| mVerticalMargin = context.getResources().getDimensionPixelSize( |
| R.dimen.payments_section_small_spacing); |
| mIconMaxWidth = context.getResources().getDimensionPixelSize( |
| R.dimen.payments_section_logo_width); |
| setSummaryText(null, null); |
| } |
| |
| @Override |
| public void handleClick(View v) { |
| // Handle click on the "ADD THING" button. |
| for (int i = 0; i < mOptionRows.size(); i++) { |
| OptionRow row = mOptionRows.get(i); |
| boolean wasClicked = row.mButton == v || row.mLabel == v || row.mIcon == v; |
| if (row.mOption == null && wasClicked) { |
| mDelegate.onAddPaymentOption(this); |
| return; |
| } |
| } |
| |
| // Update the radio button state: checked/unchecked. |
| for (int i = 0; i < mOptionRows.size(); i++) { |
| OptionRow row = mOptionRows.get(i); |
| boolean wasClicked = row.mButton == v || row.mLabel == v || row.mIcon == v; |
| if (row.mOption != null) row.setChecked(wasClicked); |
| } |
| } |
| |
| @Override |
| public void focusSection(boolean shouldFocus) { |
| // Override expansion of the section if there's no options to show. |
| boolean mayFocus = mSectionInformation != null && mSectionInformation.getSize() > 0; |
| if (!mayFocus && shouldFocus) { |
| setDisplayMode(PaymentRequestSection.DISPLAY_MODE_NORMAL); |
| return; |
| } |
| |
| super.focusSection(shouldFocus); |
| } |
| |
| @Override |
| protected boolean isLogoNecessary() { |
| return true; |
| } |
| |
| @Override |
| protected void createMainSectionContent(LinearLayout mainSectionLayout) { |
| Context context = mainSectionLayout.getContext(); |
| mCheckingProgress = createLoadingSpinner(); |
| |
| mOptionLayout = new GridLayout(context); |
| mOptionLayout.setColumnCount(3); |
| mainSectionLayout.addView(mOptionLayout, new LinearLayout.LayoutParams( |
| LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); |
| } |
| |
| /** @param canAddItems If false, this section will not show [+ ADD THING] button. */ |
| public void setCanAddItems(boolean canAddItems) { |
| mCanAddItems = canAddItems; |
| } |
| |
| /** Updates the View to account for the new {@link SectionInformation} being passed in. */ |
| public void update(SectionInformation information) { |
| mSectionInformation = information; |
| PaymentOption selectedItem = information.getSelectedItem(); |
| updateSelectedItem(selectedItem); |
| updateOptionList(information, selectedItem); |
| updateControlLayout(); |
| } |
| |
| private View createLoadingSpinner() { |
| ViewGroup spinnyLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate( |
| R.layout.payment_request_spinny, null); |
| |
| TextView textView = (TextView) spinnyLayout.findViewById(R.id.message); |
| textView.setText(getContext().getString(R.string.payments_checking_option)); |
| |
| return spinnyLayout; |
| } |
| |
| private void setSpinnerVisibility(boolean visibility) { |
| if (visibility) { |
| if (mCheckingProgress.getParent() != null) return; |
| |
| ViewGroup parent = (ViewGroup) mOptionLayout.getParent(); |
| int optionLayoutIndex = parent.indexOfChild(mOptionLayout); |
| parent.addView(mCheckingProgress, optionLayoutIndex); |
| |
| MarginLayoutParams params = |
| (MarginLayoutParams) mCheckingProgress.getLayoutParams(); |
| params.width = LayoutParams.MATCH_PARENT; |
| params.height = LayoutParams.WRAP_CONTENT; |
| params.bottomMargin = getContext().getResources().getDimensionPixelSize( |
| R.dimen.payments_section_checking_spacing); |
| mCheckingProgress.requestLayout(); |
| } else { |
| if (mCheckingProgress.getParent() == null) return; |
| |
| ViewGroup parent = (ViewGroup) mCheckingProgress.getParent(); |
| parent.removeView(mCheckingProgress); |
| } |
| } |
| |
| @Override |
| protected void updateControlLayout() { |
| if (!mIsLayoutInitialized) return; |
| |
| if (mDisplayMode == DISPLAY_MODE_FOCUSED) { |
| setIsSummaryAllowed(false); |
| mOptionLayout.setVisibility(VISIBLE); |
| setSpinnerVisibility(false); |
| } else if (mDisplayMode == DISPLAY_MODE_CHECKING) { |
| setIsSummaryAllowed(false); |
| mOptionLayout.setVisibility(GONE); |
| setSpinnerVisibility(true); |
| } else { |
| setIsSummaryAllowed(true); |
| mOptionLayout.setVisibility(GONE); |
| setSpinnerVisibility(false); |
| } |
| |
| super.updateControlLayout(); |
| } |
| |
| @Override |
| public int getEditButtonState() { |
| if (mSectionInformation == null) return EDIT_BUTTON_GONE; |
| |
| if (mSectionInformation.getSize() == 0 && mCanAddItems) { |
| // There aren't any PaymentOptions. Ask the user to add a new one. |
| return EDIT_BUTTON_ADD; |
| } else if (mSectionInformation.getSelectedItem() == null) { |
| // The user hasn't selected any available PaymentOptions. Ask the user to pick one. |
| return EDIT_BUTTON_SELECT; |
| } else { |
| return EDIT_BUTTON_GONE; |
| } |
| } |
| |
| private void updateSelectedItem(PaymentOption selectedItem) { |
| if (selectedItem == null) { |
| setLogoResource(0); |
| setIsSummaryAllowed(false); |
| setSummaryText(null, null); |
| } else { |
| setLogoResource(selectedItem.getDrawableIconId()); |
| setSummaryText(convertOptionToString(selectedItem, false), null); |
| } |
| |
| updateControlLayout(); |
| } |
| |
| private void updateOptionList(SectionInformation information, PaymentOption selectedItem) { |
| mOptionLayout.removeAllViews(); |
| mOptionRows.clear(); |
| mLabelsForTest.clear(); |
| |
| // Show any additional text requested by the layout. |
| String additionalText = mDelegate.getAdditionalText(this); |
| if (!TextUtils.isEmpty(additionalText)) { |
| OptionRow descriptionRow = new OptionRow(mOptionLayout, |
| mOptionRows.size(), |
| mDelegate.isAdditionalTextDisplayingWarning(this) |
| ? OptionRow.OPTION_ROW_TYPE_WARNING |
| : OptionRow.OPTION_ROW_TYPE_DESCRIPTION, |
| null, false); |
| mOptionRows.add(descriptionRow); |
| descriptionRow.setLabel(additionalText); |
| } |
| |
| // List out known payment options. |
| int firstOptionIndex = INVALID_OPTION_INDEX; |
| for (int i = 0; i < information.getSize(); i++) { |
| int currentRow = mOptionRows.size(); |
| if (firstOptionIndex == INVALID_OPTION_INDEX) firstOptionIndex = currentRow; |
| |
| PaymentOption item = information.getItem(i); |
| OptionRow currentOptionRow = new OptionRow(mOptionLayout, currentRow, |
| OptionRow.OPTION_ROW_TYPE_OPTION, item, item == selectedItem); |
| mOptionRows.add(currentOptionRow); |
| |
| // For testing, keep the labels in a list for easy access. |
| mLabelsForTest.add(currentOptionRow.mLabel); |
| } |
| |
| // TODO(crbug.com/627186): Find another way to give access to this resource in tests. |
| // For testing. |
| if (firstOptionIndex != INVALID_OPTION_INDEX) { |
| mOptionRows.get(firstOptionIndex).setButtonId(R.id.payments_first_radio_button); |
| } |
| |
| // If the user is allowed to add new options, show the button for it. |
| if (information.getAddStringId() != 0 && mCanAddItems) { |
| OptionRow addRow = new OptionRow(mOptionLayout, mOptionLayout.getChildCount(), |
| OptionRow.OPTION_ROW_TYPE_ADD, null, false); |
| addRow.setLabel(information.getAddStringId()); |
| addRow.setButtonId(R.id.payments_add_option_button); |
| mOptionRows.add(addRow); |
| } |
| } |
| |
| private CharSequence convertOptionToString(PaymentOption item, boolean useBoldLabel) { |
| SpannableStringBuilder builder = new SpannableStringBuilder(item.getLabel()); |
| if (useBoldLabel) { |
| builder.setSpan( |
| new StyleSpan(android.graphics.Typeface.BOLD), 0, builder.length(), 0); |
| } |
| |
| if (!TextUtils.isEmpty(item.getSublabel())) { |
| if (builder.length() > 0) builder.append("\n"); |
| builder.append(item.getSublabel()); |
| } |
| |
| if (!TextUtils.isEmpty(item.getTertiaryLabel())) { |
| if (builder.length() > 0) builder.append("\n"); |
| builder.append(item.getTertiaryLabel()); |
| } |
| |
| return builder; |
| } |
| |
| /** |
| * Returns the label at the specified |labelIndex|. Returns null if there is no label at |
| * that index. |
| */ |
| @VisibleForTesting |
| public TextView getOptionLabelsForTest(int labelIndex) { |
| return mLabelsForTest.get(labelIndex); |
| } |
| |
| /** Returns the number of option labels. */ |
| @VisibleForTesting |
| public int getNumberOfOptionLabelsForTest() { |
| return mLabelsForTest.size(); |
| } |
| |
| /** Returns the OptionRow at the specified |index|. */ |
| @VisibleForTesting |
| public OptionRow getOptionRowAtIndex(int index) { |
| return mOptionRows.get(index); |
| } |
| } |
| |
| /** |
| * Drawn as a 1dp separator. Initially drawn without being expanded to the full width of the |
| * UI, but can be expanded to separate sections fully. |
| */ |
| public static class SectionSeparator extends View { |
| /** Creates the View and adds it to the parent. */ |
| public SectionSeparator(ViewGroup parent) { |
| this(parent, -1); |
| } |
| |
| /** Creates the View and adds it to the parent at the given index. */ |
| public SectionSeparator(ViewGroup parent, int index) { |
| super(parent.getContext()); |
| Resources resources = parent.getContext().getResources(); |
| setBackgroundColor(ApiCompatibilityUtils.getColor( |
| resources, R.color.payments_section_separator)); |
| LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( |
| LayoutParams.MATCH_PARENT, |
| resources.getDimensionPixelSize(R.dimen.payments_section_separator_height)); |
| |
| int margin = resources.getDimensionPixelSize(R.dimen.payments_section_large_spacing); |
| ApiCompatibilityUtils.setMarginStart(params, margin); |
| ApiCompatibilityUtils.setMarginEnd(params, margin); |
| parent.addView(this, index, params); |
| } |
| |
| /** Expand the separator to be the full width of the dialog. */ |
| public void expand() { |
| LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams(); |
| ApiCompatibilityUtils.setMarginStart(params, 0); |
| ApiCompatibilityUtils.setMarginEnd(params, 0); |
| } |
| } |
| } |