/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts.quickcontact; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.ColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import androidx.cardview.widget.CardView; import android.text.Spannable; import android.text.TextUtils; import android.transition.ChangeBounds; import android.transition.Fade; import android.transition.Transition; import android.transition.Transition.TransitionListener; import android.transition.TransitionManager; import android.transition.TransitionSet; import android.util.AttributeSet; import android.util.Log; import android.util.Property; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import com.android.contacts.R; import com.android.contacts.dialog.CallSubjectDialog; import java.util.ArrayList; import java.util.List; /** * Display entries in a LinearLayout that can be expanded to show all entries. */ public class ExpandingEntryCardView extends CardView { private static final String TAG = "ExpandingEntryCardView"; private static final int DURATION_EXPAND_ANIMATION_FADE_IN = 200; private static final int DURATION_COLLAPSE_ANIMATION_FADE_OUT = 75; private static final int DELAY_EXPAND_ANIMATION_FADE_IN = 100; public static final int DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS = 300; public static final int DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS = 300; private static final Property VIEW_LAYOUT_HEIGHT_PROPERTY = new Property(Integer.class, "height") { @Override public void set(View view, Integer height) { LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams(); params.height = height; view.setLayoutParams(params); } @Override public Integer get(View view) { return view.getLayoutParams().height; } }; /** * Entry data. */ public static final class Entry { // No action when clicking a button is specified. public static final int ACTION_NONE = 1; // Button action is an intent. public static final int ACTION_INTENT = 2; // Button action will open the call with subject dialog. public static final int ACTION_CALL_WITH_SUBJECT = 3; private final int mId; private final Drawable mIcon; private final String mHeader; private final String mSubHeader; private final Drawable mSubHeaderIcon; private final String mText; private final Drawable mTextIcon; private Spannable mPrimaryContentDescription; private final Intent mIntent; private final Drawable mAlternateIcon; private final Intent mAlternateIntent; private Spannable mAlternateContentDescription; private final boolean mShouldApplyColor; private final boolean mIsEditable; private final EntryContextMenuInfo mEntryContextMenuInfo; private final Drawable mThirdIcon; private final Intent mThirdIntent; private final String mThirdContentDescription; private final int mIconResourceId; private final int mThirdAction; private final Bundle mThirdExtras; private final boolean mShouldApplyThirdIconColor; public Entry(int id, Drawable mainIcon, String header, String subHeader, Drawable subHeaderIcon, String text, Drawable textIcon, Spannable primaryContentDescription, Intent intent, Drawable alternateIcon, Intent alternateIntent, Spannable alternateContentDescription, boolean shouldApplyColor, boolean isEditable, EntryContextMenuInfo entryContextMenuInfo, Drawable thirdIcon, Intent thirdIntent, String thirdContentDescription, int thirdAction, Bundle thirdExtras, boolean shouldApplyThirdIconColor, int iconResourceId) { mId = id; mIcon = mainIcon; mHeader = header; mSubHeader = subHeader; mSubHeaderIcon = subHeaderIcon; mText = text; mTextIcon = textIcon; mPrimaryContentDescription = primaryContentDescription; mIntent = intent; mAlternateIcon = alternateIcon; mAlternateIntent = alternateIntent; mAlternateContentDescription = alternateContentDescription; mShouldApplyColor = shouldApplyColor; mIsEditable = isEditable; mEntryContextMenuInfo = entryContextMenuInfo; mThirdIcon = thirdIcon; mThirdIntent = thirdIntent; mThirdContentDescription = thirdContentDescription; mThirdAction = thirdAction; mThirdExtras = thirdExtras; mShouldApplyThirdIconColor = shouldApplyThirdIconColor; mIconResourceId = iconResourceId; } Drawable getIcon() { return mIcon; } String getHeader() { return mHeader; } String getSubHeader() { return mSubHeader; } Drawable getSubHeaderIcon() { return mSubHeaderIcon; } public String getText() { return mText; } Drawable getTextIcon() { return mTextIcon; } Spannable getPrimaryContentDescription() { return mPrimaryContentDescription; } Intent getIntent() { return mIntent; } Drawable getAlternateIcon() { return mAlternateIcon; } Intent getAlternateIntent() { return mAlternateIntent; } Spannable getAlternateContentDescription() { return mAlternateContentDescription; } boolean shouldApplyColor() { return mShouldApplyColor; } boolean isEditable() { return mIsEditable; } int getId() { return mId; } EntryContextMenuInfo getEntryContextMenuInfo() { return mEntryContextMenuInfo; } Drawable getThirdIcon() { return mThirdIcon; } Intent getThirdIntent() { return mThirdIntent; } String getThirdContentDescription() { return mThirdContentDescription; } int getIconResourceId() { return mIconResourceId; } public int getThirdAction() { return mThirdAction; } public Bundle getThirdExtras() { return mThirdExtras; } boolean shouldApplyThirdIconColor() { return mShouldApplyThirdIconColor; } } public interface ExpandingEntryCardViewListener { void onCollapse(int heightDelta); void onExpand(); void onExpandDone(); } private View mExpandCollapseButton; private TextView mExpandCollapseTextView; private TextView mTitleTextView; private OnClickListener mOnClickListener; private OnCreateContextMenuListener mOnCreateContextMenuListener; private boolean mIsExpanded = false; /** * The max number of entries to show in a collapsed card. If there are less entries passed in, * then they are all shown. */ private int mCollapsedEntriesCount; private ExpandingEntryCardViewListener mListener; private List> mEntries; private int mNumEntries = 0; private boolean mAllEntriesInflated = false; private List> mEntryViews; private LinearLayout mEntriesViewGroup; private final ImageView mExpandCollapseArrow; private int mThemeColor; private ColorFilter mThemeColorFilter; private boolean mIsAlwaysExpanded; /** The ViewGroup to run the expand/collapse animation on */ private ViewGroup mAnimationViewGroup; private final int mDividerLineHeightPixels; /** * List to hold the separators. This saves us from reconstructing every expand/collapse and * provides a smoother animation. */ private List mSeparators; private LinearLayout mContainer; private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() { @Override public void onClick(View v) { if (mIsExpanded) { collapse(); } else { expand(); } } }; public ExpandingEntryCardView(Context context) { this(context, null); } public ExpandingEntryCardView(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater inflater = LayoutInflater.from(context); View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this); mEntriesViewGroup = (LinearLayout) expandingEntryCardView.findViewById(R.id.content_area_linear_layout); mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title); mContainer = (LinearLayout) expandingEntryCardView.findViewById(R.id.container); mExpandCollapseButton = inflater.inflate( R.layout.quickcontact_expanding_entry_card_button, this, false); mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text); mExpandCollapseArrow = (ImageView) mExpandCollapseButton.findViewById(R.id.arrow); mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener); mDividerLineHeightPixels = getResources() .getDimensionPixelSize(R.dimen.divider_line_height); } /** * Sets the Entry list to display. * * @param entries The Entry list to display. */ public void initialize(List> entries, int numInitialVisibleEntries, boolean isExpanded, boolean isAlwaysExpanded, ExpandingEntryCardViewListener listener, ViewGroup animationViewGroup) { LayoutInflater layoutInflater = LayoutInflater.from(getContext()); mIsExpanded = isExpanded; mIsAlwaysExpanded = isAlwaysExpanded; // If isAlwaysExpanded is true, mIsExpanded should be true mIsExpanded |= mIsAlwaysExpanded; mEntryViews = new ArrayList>(entries.size()); mEntries = entries; mNumEntries = 0; mAllEntriesInflated = false; for (List entryList : mEntries) { mNumEntries += entryList.size(); mEntryViews.add(new ArrayList()); } mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries); // We need a separator between each list, but not after the last one if (entries.size() > 1) { mSeparators = new ArrayList<>(entries.size() - 1); } mListener = listener; mAnimationViewGroup = animationViewGroup; if (mIsExpanded) { updateExpandCollapseButton(getCollapseButtonText(), /* duration = */ 0); inflateAllEntries(layoutInflater); } else { updateExpandCollapseButton(getExpandButtonText(), /* duration = */ 0); inflateInitialEntries(layoutInflater); } insertEntriesIntoViewGroup(); applyColor(); } @Override public void setOnClickListener(OnClickListener listener) { mOnClickListener = listener; } @Override public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) { mOnCreateContextMenuListener = listener; } private List calculateEntriesToRemoveDuringCollapse() { final List viewsToRemove = getViewsToDisplay(true); final List viewsCollapsed = getViewsToDisplay(false); viewsToRemove.removeAll(viewsCollapsed); return viewsToRemove; } private void insertEntriesIntoViewGroup() { mEntriesViewGroup.removeAllViews(); for (View view : getViewsToDisplay(mIsExpanded)) { mEntriesViewGroup.addView(view); } removeView(mExpandCollapseButton); if (mCollapsedEntriesCount < mNumEntries && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) { mContainer.addView(mExpandCollapseButton, -1); } } /** * Returns the list of views that should be displayed. This changes depending on whether * the card is expanded or collapsed. */ private List getViewsToDisplay(boolean isExpanded) { final List viewsToDisplay = new ArrayList(); if (isExpanded) { for (int i = 0; i < mEntryViews.size(); i++) { List viewList = mEntryViews.get(i); if (i > 0) { View separator; if (mSeparators.size() <= i - 1) { separator = generateSeparator(viewList.get(0)); mSeparators.add(separator); } else { separator = mSeparators.get(i - 1); } viewsToDisplay.add(separator); } for (View view : viewList) { viewsToDisplay.add(view); } } } else { // We want to insert mCollapsedEntriesCount entries into the group. extraEntries is the // number of entries that need to be added that are not the head element of a list // to reach mCollapsedEntriesCount. int numInViewGroup = 0; int extraEntries = mCollapsedEntriesCount - mEntryViews.size(); for (int i = 0; i < mEntryViews.size() && numInViewGroup < mCollapsedEntriesCount; i++) { List entryViewList = mEntryViews.get(i); if (i > 0) { View separator; if (mSeparators.size() <= i - 1) { separator = generateSeparator(entryViewList.get(0)); mSeparators.add(separator); } else { separator = mSeparators.get(i - 1); } viewsToDisplay.add(separator); } viewsToDisplay.add(entryViewList.get(0)); numInViewGroup++; // Insert entries in this list to hit mCollapsedEntriesCount. for (int j = 1; j < entryViewList.size() && numInViewGroup < mCollapsedEntriesCount && extraEntries > 0; j++) { viewsToDisplay.add(entryViewList.get(j)); numInViewGroup++; extraEntries--; } } } formatEntryIfFirst(viewsToDisplay); return viewsToDisplay; } private void formatEntryIfFirst(List entriesViewGroup) { // If no title and the first entry in the group, add extra padding if (TextUtils.isEmpty(mTitleTextView.getText()) && entriesViewGroup.size() > 0) { final View entry = entriesViewGroup.get(0); entry.setPaddingRelative(entry.getPaddingStart(), getResources().getDimensionPixelSize( R.dimen.expanding_entry_card_item_padding_top) + getResources().getDimensionPixelSize( R.dimen.expanding_entry_card_null_title_top_extra_padding), entry.getPaddingEnd(), entry.getPaddingBottom()); } } private View generateSeparator(View entry) { View separator = new View(getContext()); Resources res = getResources(); separator.setBackgroundColor(res.getColor( R.color.divider_line_color_light)); LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, mDividerLineHeightPixels); // The separator is aligned with the text in the entry. This is offset by a default // margin. If there is an icon present, the icon's width and margin are added int marginStart = res.getDimensionPixelSize( R.dimen.expanding_entry_card_item_padding_start); ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon); if (entryIcon.getVisibility() == View.VISIBLE) { int imageWidthAndMargin = res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_icon_width) + res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_image_spacing); marginStart += imageWidthAndMargin; } layoutParams.setMarginStart(marginStart); separator.setLayoutParams(layoutParams); return separator; } private CharSequence getExpandButtonText() { // Default to "See more". return getResources().getText(R.string.expanding_entry_card_view_see_more); } private CharSequence getCollapseButtonText() { // Default to "See less". return getResources().getText(R.string.expanding_entry_card_view_see_less); } /** * Inflates the initial entries to be shown. */ private void inflateInitialEntries(LayoutInflater layoutInflater) { // If the number of collapsed entries equals total entries, inflate all if (mCollapsedEntriesCount == mNumEntries) { inflateAllEntries(layoutInflater); } else { // Otherwise inflate the top entry from each list // extraEntries is used to add extra entries until mCollapsedEntriesCount is reached. int numInflated = 0; int extraEntries = mCollapsedEntriesCount - mEntries.size(); for (int i = 0; i < mEntries.size() && numInflated < mCollapsedEntriesCount; i++) { List entryList = mEntries.get(i); List entryViewList = mEntryViews.get(i); entryViewList.add(createEntryView(layoutInflater, entryList.get(0), /* showIcon = */ View.VISIBLE)); numInflated++; // Inflate entries in this list to hit mCollapsedEntriesCount. for (int j = 1; j < entryList.size() && numInflated < mCollapsedEntriesCount && extraEntries > 0; j++) { entryViewList.add(createEntryView(layoutInflater, entryList.get(j), /* showIcon = */ View.INVISIBLE)); numInflated++; extraEntries--; } } } } /** * Inflates all entries. */ private void inflateAllEntries(LayoutInflater layoutInflater) { if (mAllEntriesInflated) { return; } for (int i = 0; i < mEntries.size(); i++) { List entryList = mEntries.get(i); List viewList = mEntryViews.get(i); for (int j = viewList.size(); j < entryList.size(); j++) { final int iconVisibility; final Entry entry = entryList.get(j); // If the entry does not have an icon, mark gone. Else if it has an icon, show // for the first Entry in the list only if (entry.getIcon() == null) { iconVisibility = View.GONE; } else if (j == 0) { iconVisibility = View.VISIBLE; } else { iconVisibility = View.INVISIBLE; } viewList.add(createEntryView(layoutInflater, entry, iconVisibility)); } } mAllEntriesInflated = true; } public void setColorAndFilter(int color, ColorFilter colorFilter) { mThemeColor = color; mThemeColorFilter = colorFilter; applyColor(); } public void setEntryHeaderColor(int color) { if (mEntries != null) { for (List entryList : mEntryViews) { for (View entryView : entryList) { TextView header = (TextView) entryView.findViewById(R.id.header); if (header != null) { header.setTextColor(color); } } } } } public void setEntrySubHeaderColor(int color) { if (mEntries != null) { for (List entryList : mEntryViews) { for (View entryView : entryList) { final TextView subHeader = (TextView) entryView.findViewById(R.id.sub_header); if (subHeader != null) { subHeader.setTextColor(color); } } } } } /** * The ColorFilter is passed in along with the color so that a new one only needs to be created * once for the entire activity. * 1. Title * 2. Entry icons * 3. Expand/Collapse Text * 4. Expand/Collapse Button */ public void applyColor() { if (mThemeColor != 0 && mThemeColorFilter != null) { // Title if (mTitleTextView != null) { mTitleTextView.setTextColor(mThemeColor); } // Entry icons if (mEntries != null) { for (List entryList : mEntries) { for (Entry entry : entryList) { if (entry.shouldApplyColor()) { Drawable icon = entry.getIcon(); if (icon != null) { icon.mutate(); icon.setColorFilter(mThemeColorFilter); } } Drawable alternateIcon = entry.getAlternateIcon(); if (alternateIcon != null) { alternateIcon.mutate(); alternateIcon.setColorFilter(mThemeColorFilter); } Drawable thirdIcon = entry.getThirdIcon(); if (thirdIcon != null && entry.shouldApplyThirdIconColor()) { thirdIcon.mutate(); thirdIcon.setColorFilter(mThemeColorFilter); } } } } // Expand/Collapse mExpandCollapseTextView.setTextColor(mThemeColor); mExpandCollapseArrow.setColorFilter(mThemeColorFilter); } } private View createEntryView(LayoutInflater layoutInflater, final Entry entry, int iconVisibility) { final EntryView view = (EntryView) layoutInflater.inflate( R.layout.expanding_entry_card_item, this, false); view.setContextMenuInfo(entry.getEntryContextMenuInfo()); if (!TextUtils.isEmpty(entry.getPrimaryContentDescription())) { view.setContentDescription(entry.getPrimaryContentDescription()); } final ImageView icon = (ImageView) view.findViewById(R.id.icon); icon.setVisibility(iconVisibility); if (entry.getIcon() != null) { icon.setImageDrawable(entry.getIcon()); } final TextView header = (TextView) view.findViewById(R.id.header); if (!TextUtils.isEmpty(entry.getHeader())) { header.setText(entry.getHeader()); } else { header.setVisibility(View.GONE); } final TextView subHeader = (TextView) view.findViewById(R.id.sub_header); if (!TextUtils.isEmpty(entry.getSubHeader())) { subHeader.setText(entry.getSubHeader()); } else { subHeader.setVisibility(View.GONE); } final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header); if (entry.getSubHeaderIcon() != null) { subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon()); } else { subHeaderIcon.setVisibility(View.GONE); } final TextView text = (TextView) view.findViewById(R.id.text); if (!TextUtils.isEmpty(entry.getText())) { text.setText(entry.getText()); } else { text.setVisibility(View.GONE); } final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text); if (entry.getTextIcon() != null) { textIcon.setImageDrawable(entry.getTextIcon()); } else { textIcon.setVisibility(View.GONE); } if (entry.getIntent() != null) { view.setOnClickListener(mOnClickListener); view.setTag(new EntryTag(entry.getId(), entry.getIntent())); } if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) { // Remove the click effect view.setBackground(null); } // If only the header is visible, add a top margin to match icon's top margin. // Also increase the space below the header for visual comfort. if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE && text.getVisibility() == View.GONE) { RelativeLayout.LayoutParams headerLayoutParams = (RelativeLayout.LayoutParams) header.getLayoutParams(); headerLayoutParams.topMargin = (int) (getResources().getDimension( R.dimen.expanding_entry_card_item_header_only_margin_top)); headerLayoutParams.bottomMargin += (int) (getResources().getDimension( R.dimen.expanding_entry_card_item_header_only_margin_bottom)); header.setLayoutParams(headerLayoutParams); } // Adjust the top padding size for entries with an invisible icon. The padding depends on // if there is a sub header or text section if (iconVisibility == View.INVISIBLE && (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) { view.setPaddingRelative(view.getPaddingStart(), getResources().getDimensionPixelSize( R.dimen.expanding_entry_card_item_no_icon_margin_top), view.getPaddingEnd(), view.getPaddingBottom()); } else if (iconVisibility == View.INVISIBLE && TextUtils.isEmpty(entry.getSubHeader()) && TextUtils.isEmpty(entry.getText())) { view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(), view.getPaddingBottom()); } final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate); final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon); if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) { alternateIcon.setImageDrawable(entry.getAlternateIcon()); alternateIcon.setOnClickListener(mOnClickListener); alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent())); alternateIcon.setVisibility(View.VISIBLE); alternateIcon.setContentDescription(entry.getAlternateContentDescription()); } if (entry.getThirdIcon() != null && entry.getThirdAction() != Entry.ACTION_NONE) { thirdIcon.setImageDrawable(entry.getThirdIcon()); if (entry.getThirdAction() == Entry.ACTION_INTENT) { thirdIcon.setOnClickListener(mOnClickListener); thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent())); } else if (entry.getThirdAction() == Entry.ACTION_CALL_WITH_SUBJECT) { thirdIcon.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Object tag = v.getTag(); if (!(tag instanceof Bundle)) { return; } Context context = getContext(); if (context instanceof Activity) { CallSubjectDialog.start((Activity) context, entry.getThirdExtras()); } } }); thirdIcon.setTag(entry.getThirdExtras()); } thirdIcon.setVisibility(View.VISIBLE); thirdIcon.setContentDescription(entry.getThirdContentDescription()); } // Set a custom touch listener for expanding the extra icon touch areas view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon)); view.setOnCreateContextMenuListener(mOnCreateContextMenuListener); return view; } private void updateExpandCollapseButton(CharSequence buttonText, long duration) { if (mIsExpanded) { final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, "rotation", 180); animator.setDuration(duration); animator.start(); } else { final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow, "rotation", 0); animator.setDuration(duration); animator.start(); } mExpandCollapseTextView.setText(buttonText); } private void expand() { ChangeBounds boundsTransition = new ChangeBounds(); boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); Fade fadeIn = new Fade(Fade.IN); fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN); fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN); TransitionSet transitionSet = new TransitionSet(); transitionSet.addTransition(boundsTransition); transitionSet.addTransition(fadeIn); transitionSet.excludeTarget(R.id.text, /* exclude = */ true); final ViewGroup transitionViewContainer = mAnimationViewGroup == null ? this : mAnimationViewGroup; transitionSet.addListener(new TransitionListener() { @Override public void onTransitionStart(Transition transition) { mListener.onExpand(); } @Override public void onTransitionEnd(Transition transition) { mListener.onExpandDone(); } @Override public void onTransitionCancel(Transition transition) { } @Override public void onTransitionPause(Transition transition) { } @Override public void onTransitionResume(Transition transition) { } }); TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet); mIsExpanded = true; // In order to insert new entries, we may need to inflate them for the first time inflateAllEntries(LayoutInflater.from(getContext())); insertEntriesIntoViewGroup(); updateExpandCollapseButton(getCollapseButtonText(), DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS); } private void collapse() { final List views = calculateEntriesToRemoveDuringCollapse(); // This animation requires layout changes, unlike the expand() animation: the action bar // might get scrolled open in order to fill empty space. As a result, we can't use // ChangeBounds here. Instead manually animate view height and alpha. This isn't as // efficient as the bounds and translation changes performed by ChangeBounds. Nonetheless, a // reasonable frame-rate is achieved collapsing a dozen elements on a user Svelte N4. So the // performance hit doesn't justify writing a less maintainable animation. final AnimatorSet set = new AnimatorSet(); final List animators = new ArrayList(views.size()); int totalSizeChange = 0; for (View viewToRemove : views) { final ObjectAnimator animator = ObjectAnimator.ofObject(viewToRemove, VIEW_LAYOUT_HEIGHT_PROPERTY, null, viewToRemove.getHeight(), 0); totalSizeChange += viewToRemove.getHeight(); animator.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); animators.add(animator); viewToRemove.animate().alpha(0).setDuration(DURATION_COLLAPSE_ANIMATION_FADE_OUT); } set.playTogether(animators); set.start(); set.addListener(new AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { // Now that the views have been animated away, actually remove them from the view // hierarchy. Reset their appearance so that they look appropriate when they // get added back later. insertEntriesIntoViewGroup(); for (View view : views) { if (view instanceof EntryView) { VIEW_LAYOUT_HEIGHT_PROPERTY.set(view, LayoutParams.WRAP_CONTENT); } else { VIEW_LAYOUT_HEIGHT_PROPERTY.set(view, mDividerLineHeightPixels); } view.animate().cancel(); view.setAlpha(1); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); mListener.onCollapse(totalSizeChange); mIsExpanded = false; updateExpandCollapseButton(getExpandButtonText(), DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS); } /** * Returns whether the view is currently in its expanded state. */ public boolean isExpanded() { return mIsExpanded; } /** * Sets the title text of this ExpandingEntryCardView. * @param title The title to set. A null title will result in the title being removed. */ public void setTitle(String title) { if (mTitleTextView == null) { Log.e(TAG, "mTitleTextView is null"); } mTitleTextView.setText(title); mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE); findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE); // If the title is set after children have been added, reset the top entry's padding to // the default. Else if the title is cleared after children have been added, set // the extra top padding if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { View firstEntry = mEntriesViewGroup.getChildAt(0); firstEntry.setPadding(firstEntry.getPaddingLeft(), getResources().getDimensionPixelSize( R.dimen.expanding_entry_card_item_padding_top), firstEntry.getPaddingRight(), firstEntry.getPaddingBottom()); } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) { View firstEntry = mEntriesViewGroup.getChildAt(0); firstEntry.setPadding(firstEntry.getPaddingLeft(), getResources().getDimensionPixelSize( R.dimen.expanding_entry_card_item_padding_top) + getResources().getDimensionPixelSize( R.dimen.expanding_entry_card_null_title_top_extra_padding), firstEntry.getPaddingRight(), firstEntry.getPaddingBottom()); } } public boolean shouldShow() { return mEntries != null && mEntries.size() > 0; } public static final class EntryView extends RelativeLayout { private EntryContextMenuInfo mEntryContextMenuInfo; public EntryView(Context context) { super(context); } public EntryView(Context context, AttributeSet attrs) { super(context, attrs); } public void setContextMenuInfo(EntryContextMenuInfo info) { mEntryContextMenuInfo = info; } @Override protected ContextMenuInfo getContextMenuInfo() { return mEntryContextMenuInfo; } } public static final class EntryContextMenuInfo implements ContextMenuInfo { private final String mCopyText; private final String mCopyLabel; private final String mMimeType; private final long mId; private final boolean mIsSuperPrimary; public EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id, boolean isSuperPrimary) { mCopyText = copyText; mCopyLabel = copyLabel; mMimeType = mimeType; mId = id; mIsSuperPrimary = isSuperPrimary; } public String getCopyText() { return mCopyText; } public String getCopyLabel() { return mCopyLabel; } public String getMimeType() { return mMimeType; } public long getId() { return mId; } public boolean isSuperPrimary() { return mIsSuperPrimary; } } static final class EntryTag { private final int mId; private final Intent mIntent; public EntryTag(int id, Intent intent) { mId = id; mIntent = intent; } public int getId() { return mId; } public Intent getIntent() { return mIntent; } } /** * This custom touch listener increases the touch area for the second and third icons, if * they are present. This is necessary to maintain other properties on an entry view, like * using a top padding on entry. Based off of {@link android.view.TouchDelegate} */ private static final class EntryTouchListener implements View.OnTouchListener { private final View mEntry; private final ImageView mAlternateIcon; private final ImageView mThirdIcon; /** mTouchedView locks in a view on touch down */ private View mTouchedView; /** mSlop adds some space to account for touches that are just outside the hit area */ private int mSlop; public EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon) { mEntry = entry; mAlternateIcon = alternateIcon; mThirdIcon = thirdIcon; mSlop = ViewConfiguration.get(entry.getContext()).getScaledTouchSlop(); } @Override public boolean onTouch(View v, MotionEvent event) { View touchedView = mTouchedView; boolean sendToTouched = false; boolean hit = true; boolean handled = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (hitThirdIcon(event)) { mTouchedView = mThirdIcon; sendToTouched = true; } else if (hitAlternateIcon(event)) { mTouchedView = mAlternateIcon; sendToTouched = true; } else { mTouchedView = mEntry; sendToTouched = false; } touchedView = mTouchedView; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_MOVE: sendToTouched = mTouchedView != null && mTouchedView != mEntry; if (sendToTouched) { final Rect slopBounds = new Rect(); touchedView.getHitRect(slopBounds); slopBounds.inset(-mSlop, -mSlop); if (!slopBounds.contains((int) event.getX(), (int) event.getY())) { hit = false; } } break; case MotionEvent.ACTION_CANCEL: sendToTouched = mTouchedView != null && mTouchedView != mEntry; mTouchedView = null; break; } if (sendToTouched) { if (hit) { event.setLocation(touchedView.getWidth() / 2, touchedView.getHeight() / 2); } else { // Offset event coordinates to be outside the target view (in case it does // something like tracking pressed state) event.setLocation(-(mSlop * 2), -(mSlop * 2)); } handled = touchedView.dispatchTouchEvent(event); } return handled; } private boolean hitThirdIcon(MotionEvent event) { if (mEntry.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { return mThirdIcon.getVisibility() == View.VISIBLE && event.getX() < mThirdIcon.getRight(); } else { return mThirdIcon.getVisibility() == View.VISIBLE && event.getX() > mThirdIcon.getLeft(); } } /** * Should be used after checking if third icon was hit */ private boolean hitAlternateIcon(MotionEvent event) { // LayoutParams used to add the start margin to the touch area final RelativeLayout.LayoutParams alternateIconParams = (RelativeLayout.LayoutParams) mAlternateIcon.getLayoutParams(); if (mEntry.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { return mAlternateIcon.getVisibility() == View.VISIBLE && event.getX() < mAlternateIcon.getRight() + alternateIconParams.rightMargin; } else { return mAlternateIcon.getVisibility() == View.VISIBLE && event.getX() > mAlternateIcon.getLeft() - alternateIconParams.leftMargin; } } } }