blob: 49da4c811709f718682fccb9dbb4a39bb7291f37 [file] [log] [blame]
Paul Soulos2d48b5a2014-05-29 13:56:25 -07001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.contacts.quickcontact;
17
Brian Attwell245d3d22015-01-21 09:50:08 -080018import android.animation.Animator;
19import android.animation.Animator.AnimatorListener;
20import android.animation.AnimatorSet;
Paul Soulosc205cf12014-08-04 14:35:56 -070021import android.animation.ObjectAnimator;
Tyler Gunn5f87e922015-08-05 14:24:52 -070022import android.app.Activity;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070023import android.content.Context;
24import android.content.Intent;
25import android.content.res.Resources;
Paul Soulos7b0b0ce2014-06-24 14:26:34 -070026import android.graphics.ColorFilter;
Paul Soulosdd7419d2014-07-15 11:22:13 -070027import android.graphics.Rect;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070028import android.graphics.drawable.Drawable;
Tyler Gunn5f87e922015-08-05 14:24:52 -070029import android.os.Bundle;
Paul Soulosac9b3162014-07-30 16:45:01 -070030import android.support.v7.widget.CardView;
Walter Jang7ce53522014-10-29 13:26:43 -070031import android.text.Spannable;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070032import android.text.TextUtils;
Paul Soulos0cda9ae2014-07-23 11:27:28 -070033import android.transition.ChangeBounds;
Paul Soulos0cda9ae2014-07-23 11:27:28 -070034import android.transition.Fade;
35import android.transition.Transition;
36import android.transition.Transition.TransitionListener;
37import android.transition.TransitionManager;
38import android.transition.TransitionSet;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070039import android.util.AttributeSet;
40import android.util.Log;
Brian Attwell245d3d22015-01-21 09:50:08 -080041import android.util.Property;
Paul Soulos2a4207f2014-07-31 17:09:05 -070042import android.view.ContextMenu.ContextMenuInfo;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070043import android.view.LayoutInflater;
Paul Soulos48fc9122014-08-26 13:52:36 -070044import android.view.MotionEvent;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070045import android.view.View;
Paul Soulos48fc9122014-08-26 13:52:36 -070046import android.view.ViewConfiguration;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070047import android.view.ViewGroup;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070048import android.widget.ImageView;
49import android.widget.LinearLayout;
Paul Soulos4b943552014-07-23 14:49:52 -070050import android.widget.RelativeLayout;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070051import android.widget.TextView;
52
Paul Soulos0cda9ae2014-07-23 11:27:28 -070053import com.android.contacts.R;
Gary Mai69c182a2016-12-05 13:07:03 -080054import com.android.contacts.dialog.CallSubjectDialog;
Paul Soulos0cda9ae2014-07-23 11:27:28 -070055
Paul Soulos2d48b5a2014-05-29 13:56:25 -070056import java.util.ArrayList;
57import java.util.List;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070058
59/**
60 * Display entries in a LinearLayout that can be expanded to show all entries.
61 */
Paul Soulosac9b3162014-07-30 16:45:01 -070062public class ExpandingEntryCardView extends CardView {
Paul Soulos2d48b5a2014-05-29 13:56:25 -070063
64 private static final String TAG = "ExpandingEntryCardView";
Paul Soulos0cda9ae2014-07-23 11:27:28 -070065 private static final int DURATION_EXPAND_ANIMATION_FADE_IN = 200;
Brian Attwell245d3d22015-01-21 09:50:08 -080066 private static final int DURATION_COLLAPSE_ANIMATION_FADE_OUT = 75;
Paul Soulos0cda9ae2014-07-23 11:27:28 -070067 private static final int DELAY_EXPAND_ANIMATION_FADE_IN = 100;
68
69 public static final int DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS = 300;
70 public static final int DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS = 300;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070071
Brian Attwell245d3d22015-01-21 09:50:08 -080072 private static final Property<View, Integer> VIEW_LAYOUT_HEIGHT_PROPERTY =
73 new Property<View, Integer>(Integer.class, "height") {
74 @Override
75 public void set(View view, Integer height) {
76 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
77 view.getLayoutParams();
78 params.height = height;
79 view.setLayoutParams(params);
80 }
81
82 @Override
83 public Integer get(View view) {
84 return view.getLayoutParams().height;
85 }
86 };
87
Paul Soulos2d48b5a2014-05-29 13:56:25 -070088 /**
89 * Entry data.
90 */
91 public static final class Entry {
Tyler Gunn5f87e922015-08-05 14:24:52 -070092 // No action when clicking a button is specified.
93 public static final int ACTION_NONE = 1;
94 // Button action is an intent.
95 public static final int ACTION_INTENT = 2;
96 // Button action will open the call with subject dialog.
97 public static final int ACTION_CALL_WITH_SUBJECT = 3;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070098
Paul Soulos2ed2a732014-08-12 11:58:39 -070099 private final int mId;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700100 private final Drawable mIcon;
101 private final String mHeader;
102 private final String mSubHeader;
103 private final Drawable mSubHeaderIcon;
104 private final String mText;
105 private final Drawable mTextIcon;
Walter Jang7ce53522014-10-29 13:26:43 -0700106 private Spannable mPrimaryContentDescription;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700107 private final Intent mIntent;
Paul Soulosdd7419d2014-07-15 11:22:13 -0700108 private final Drawable mAlternateIcon;
109 private final Intent mAlternateIntent;
Wenyi Wang5da55ff2015-11-19 13:22:40 -0800110 private Spannable mAlternateContentDescription;
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700111 private final boolean mShouldApplyColor;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700112 private final boolean mIsEditable;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700113 private final EntryContextMenuInfo mEntryContextMenuInfo;
Paul Soulos48fc9122014-08-26 13:52:36 -0700114 private final Drawable mThirdIcon;
115 private final Intent mThirdIntent;
116 private final String mThirdContentDescription;
Paul Soulos48290be2014-09-08 13:44:51 -0700117 private final int mIconResourceId;
Tyler Gunn5f87e922015-08-05 14:24:52 -0700118 private final int mThirdAction;
119 private final Bundle mThirdExtras;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700120
Paul Soulos2ed2a732014-08-12 11:58:39 -0700121 public Entry(int id, Drawable mainIcon, String header, String subHeader,
Paul Soulos23e28362014-08-29 14:57:08 -0700122 Drawable subHeaderIcon, String text, Drawable textIcon,
Walter Jang7ce53522014-10-29 13:26:43 -0700123 Spannable primaryContentDescription, Intent intent,
Wenyi Wang5da55ff2015-11-19 13:22:40 -0800124 Drawable alternateIcon, Intent alternateIntent,
125 Spannable alternateContentDescription, boolean shouldApplyColor, boolean isEditable,
Paul Soulos48fc9122014-08-26 13:52:36 -0700126 EntryContextMenuInfo entryContextMenuInfo, Drawable thirdIcon, Intent thirdIntent,
Tyler Gunn5f87e922015-08-05 14:24:52 -0700127 String thirdContentDescription, int thirdAction, Bundle thirdExtras,
128 int iconResourceId) {
Paul Soulos2ed2a732014-08-12 11:58:39 -0700129 mId = id;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700130 mIcon = mainIcon;
131 mHeader = header;
132 mSubHeader = subHeader;
133 mSubHeaderIcon = subHeaderIcon;
134 mText = text;
135 mTextIcon = textIcon;
Paul Soulos23e28362014-08-29 14:57:08 -0700136 mPrimaryContentDescription = primaryContentDescription;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700137 mIntent = intent;
Paul Soulosdd7419d2014-07-15 11:22:13 -0700138 mAlternateIcon = alternateIcon;
139 mAlternateIntent = alternateIntent;
140 mAlternateContentDescription = alternateContentDescription;
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700141 mShouldApplyColor = shouldApplyColor;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700142 mIsEditable = isEditable;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700143 mEntryContextMenuInfo = entryContextMenuInfo;
Paul Soulos48fc9122014-08-26 13:52:36 -0700144 mThirdIcon = thirdIcon;
145 mThirdIntent = thirdIntent;
146 mThirdContentDescription = thirdContentDescription;
Tyler Gunn5f87e922015-08-05 14:24:52 -0700147 mThirdAction = thirdAction;
148 mThirdExtras = thirdExtras;
Paul Soulos48290be2014-09-08 13:44:51 -0700149 mIconResourceId = iconResourceId;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700150 }
151
152 Drawable getIcon() {
153 return mIcon;
154 }
155
156 String getHeader() {
157 return mHeader;
158 }
159
160 String getSubHeader() {
161 return mSubHeader;
162 }
163
164 Drawable getSubHeaderIcon() {
165 return mSubHeaderIcon;
166 }
167
168 public String getText() {
169 return mText;
170 }
171
172 Drawable getTextIcon() {
173 return mTextIcon;
174 }
175
Walter Jang7ce53522014-10-29 13:26:43 -0700176 Spannable getPrimaryContentDescription() {
Paul Soulos23e28362014-08-29 14:57:08 -0700177 return mPrimaryContentDescription;
178 }
179
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700180 Intent getIntent() {
181 return mIntent;
182 }
183
Paul Soulosdd7419d2014-07-15 11:22:13 -0700184 Drawable getAlternateIcon() {
185 return mAlternateIcon;
186 }
187
188 Intent getAlternateIntent() {
189 return mAlternateIntent;
190 }
191
Wenyi Wang5da55ff2015-11-19 13:22:40 -0800192 Spannable getAlternateContentDescription() {
Paul Soulosdd7419d2014-07-15 11:22:13 -0700193 return mAlternateContentDescription;
194 }
195
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700196 boolean shouldApplyColor() {
197 return mShouldApplyColor;
198 }
199
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700200 boolean isEditable() {
201 return mIsEditable;
202 }
Paul Soulosea5e0b72014-07-08 18:09:44 -0700203
Paul Soulos2ed2a732014-08-12 11:58:39 -0700204 int getId() {
205 return mId;
Paul Soulosea5e0b72014-07-08 18:09:44 -0700206 }
Paul Soulos2a4207f2014-07-31 17:09:05 -0700207
208 EntryContextMenuInfo getEntryContextMenuInfo() {
209 return mEntryContextMenuInfo;
210 }
Paul Soulos48fc9122014-08-26 13:52:36 -0700211
212 Drawable getThirdIcon() {
213 return mThirdIcon;
214 }
215
216 Intent getThirdIntent() {
217 return mThirdIntent;
218 }
219
220 String getThirdContentDescription() {
221 return mThirdContentDescription;
222 }
Paul Soulos48290be2014-09-08 13:44:51 -0700223
224 int getIconResourceId() {
225 return mIconResourceId;
226 }
Tyler Gunn5f87e922015-08-05 14:24:52 -0700227
228 public int getThirdAction() {
229 return mThirdAction;
230 }
231
232 public Bundle getThirdExtras() {
233 return mThirdExtras;
234 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700235 }
236
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700237 public interface ExpandingEntryCardViewListener {
238 void onCollapse(int heightDelta);
Brian Attwell245d3d22015-01-21 09:50:08 -0800239 void onExpand();
240 void onExpandDone();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700241 }
242
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700243 private View mExpandCollapseButton;
244 private TextView mExpandCollapseTextView;
245 private TextView mTitleTextView;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700246 private OnClickListener mOnClickListener;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700247 private OnCreateContextMenuListener mOnCreateContextMenuListener;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700248 private boolean mIsExpanded = false;
Paul Soulos691dd8f2014-08-13 16:10:43 -0700249 /**
250 * The max number of entries to show in a collapsed card. If there are less entries passed in,
251 * then they are all shown.
252 */
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700253 private int mCollapsedEntriesCount;
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700254 private ExpandingEntryCardViewListener mListener;
Paul Soulos60e51082014-07-10 12:33:04 -0700255 private List<List<Entry>> mEntries;
256 private int mNumEntries = 0;
257 private boolean mAllEntriesInflated = false;
258 private List<List<View>> mEntryViews;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700259 private LinearLayout mEntriesViewGroup;
Paul Soulosc205cf12014-08-04 14:35:56 -0700260 private final ImageView mExpandCollapseArrow;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700261 private int mThemeColor;
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700262 private ColorFilter mThemeColorFilter;
Paul Soulos89966b42014-07-21 12:38:50 -0700263 private boolean mIsAlwaysExpanded;
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700264 /** The ViewGroup to run the expand/collapse animation on */
265 private ViewGroup mAnimationViewGroup;
Brian Attwell05287bf2015-02-25 22:24:04 -0800266 private final int mDividerLineHeightPixels;
Paul Souloscc5ec222014-08-25 12:02:26 -0700267 /**
268 * List to hold the separators. This saves us from reconstructing every expand/collapse and
269 * provides a smoother animation.
270 */
271 private List<View> mSeparators;
Paul Soulosac9b3162014-07-30 16:45:01 -0700272 private LinearLayout mContainer;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700273
274 private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() {
275 @Override
276 public void onClick(View v) {
277 if (mIsExpanded) {
278 collapse();
279 } else {
280 expand();
281 }
282 }
283 };
284
285 public ExpandingEntryCardView(Context context) {
Brian Attwell0d49d812014-06-06 18:01:01 -0700286 this(context, null);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700287 }
288
289 public ExpandingEntryCardView(Context context, AttributeSet attrs) {
290 super(context, attrs);
291 LayoutInflater inflater = LayoutInflater.from(context);
292 View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this);
293 mEntriesViewGroup = (LinearLayout)
294 expandingEntryCardView.findViewById(R.id.content_area_linear_layout);
295 mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title);
Paul Soulosac9b3162014-07-30 16:45:01 -0700296 mContainer = (LinearLayout) expandingEntryCardView.findViewById(R.id.container);
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700297
298 mExpandCollapseButton = inflater.inflate(
299 R.layout.quickcontact_expanding_entry_card_button, this, false);
300 mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text);
Paul Soulosc205cf12014-08-04 14:35:56 -0700301 mExpandCollapseArrow = (ImageView) mExpandCollapseButton.findViewById(R.id.arrow);
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700302 mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener);
Brian Attwell05287bf2015-02-25 22:24:04 -0800303 mDividerLineHeightPixels = getResources()
304 .getDimensionPixelSize(R.dimen.divider_line_height);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700305 }
306
307 /**
308 * Sets the Entry list to display.
309 *
310 * @param entries The Entry list to display.
311 */
Paul Soulos60e51082014-07-10 12:33:04 -0700312 public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries,
yaolu139a03b2016-09-02 17:44:10 -0700313 boolean isExpanded, boolean isAlwaysExpanded, ExpandingEntryCardViewListener listener,
314 ViewGroup animationViewGroup) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700315 LayoutInflater layoutInflater = LayoutInflater.from(getContext());
316 mIsExpanded = isExpanded;
Paul Soulos89966b42014-07-21 12:38:50 -0700317 mIsAlwaysExpanded = isAlwaysExpanded;
318 // If isAlwaysExpanded is true, mIsExpanded should be true
319 mIsExpanded |= mIsAlwaysExpanded;
Paul Soulos60e51082014-07-10 12:33:04 -0700320 mEntryViews = new ArrayList<List<View>>(entries.size());
Brian Attwell0d49d812014-06-06 18:01:01 -0700321 mEntries = entries;
Brian Attwell60953692014-07-11 17:18:46 -0700322 mNumEntries = 0;
323 mAllEntriesInflated = false;
Paul Soulos60e51082014-07-10 12:33:04 -0700324 for (List<Entry> entryList : mEntries) {
325 mNumEntries += entryList.size();
326 mEntryViews.add(new ArrayList<View>());
327 }
328 mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries);
Paul Souloscc5ec222014-08-25 12:02:26 -0700329 // We need a separator between each list, but not after the last one
330 if (entries.size() > 1) {
331 mSeparators = new ArrayList<>(entries.size() - 1);
332 }
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700333 mListener = listener;
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700334 mAnimationViewGroup = animationViewGroup;
Brian Attwell0d49d812014-06-06 18:01:01 -0700335
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700336 if (mIsExpanded) {
Paul Soulosc205cf12014-08-04 14:35:56 -0700337 updateExpandCollapseButton(getCollapseButtonText(), /* duration = */ 0);
Paul Soulos60e51082014-07-10 12:33:04 -0700338 inflateAllEntries(layoutInflater);
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700339 } else {
Paul Soulosc205cf12014-08-04 14:35:56 -0700340 updateExpandCollapseButton(getExpandButtonText(), /* duration = */ 0);
Paul Soulos60e51082014-07-10 12:33:04 -0700341 inflateInitialEntries(layoutInflater);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700342 }
343 insertEntriesIntoViewGroup();
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700344 applyColor();
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700345 }
346
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700347 @Override
348 public void setOnClickListener(OnClickListener listener) {
349 mOnClickListener = listener;
350 }
351
Paul Soulos2a4207f2014-07-31 17:09:05 -0700352 @Override
353 public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) {
354 mOnCreateContextMenuListener = listener;
355 }
356
Brian Attwell245d3d22015-01-21 09:50:08 -0800357 private List<View> calculateEntriesToRemoveDuringCollapse() {
358 final List<View> viewsToRemove = getViewsToDisplay(true);
359 final List<View> viewsCollapsed = getViewsToDisplay(false);
360 viewsToRemove.removeAll(viewsCollapsed);
361 return viewsToRemove;
362 }
363
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700364 private void insertEntriesIntoViewGroup() {
365 mEntriesViewGroup.removeAllViews();
Paul Soulos60e51082014-07-10 12:33:04 -0700366
Brian Attwell245d3d22015-01-21 09:50:08 -0800367 for (View view : getViewsToDisplay(mIsExpanded)) {
368 mEntriesViewGroup.addView(view);
369 }
370
371 removeView(mExpandCollapseButton);
372 if (mCollapsedEntriesCount < mNumEntries
373 && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) {
374 mContainer.addView(mExpandCollapseButton, -1);
375 }
376 }
377
378 /**
379 * Returns the list of views that should be displayed. This changes depending on whether
380 * the card is expanded or collapsed.
381 */
382 private List<View> getViewsToDisplay(boolean isExpanded) {
383 final List<View> viewsToDisplay = new ArrayList<View>();
384 if (isExpanded) {
Paul Souloscc5ec222014-08-25 12:02:26 -0700385 for (int i = 0; i < mEntryViews.size(); i++) {
386 List<View> viewList = mEntryViews.get(i);
387 if (i > 0) {
388 View separator;
389 if (mSeparators.size() <= i - 1) {
390 separator = generateSeparator(viewList.get(0));
391 mSeparators.add(separator);
392 } else {
393 separator = mSeparators.get(i - 1);
394 }
Brian Attwell245d3d22015-01-21 09:50:08 -0800395 viewsToDisplay.add(separator);
Paul Soulos97ed5012014-07-28 16:27:12 -0700396 }
Paul Soulos60e51082014-07-10 12:33:04 -0700397 for (View view : viewList) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800398 viewsToDisplay.add(view);
Paul Soulos60e51082014-07-10 12:33:04 -0700399 }
400 }
401 } else {
Paul Soulos691dd8f2014-08-13 16:10:43 -0700402 // We want to insert mCollapsedEntriesCount entries into the group. extraEntries is the
403 // number of entries that need to be added that are not the head element of a list
404 // to reach mCollapsedEntriesCount.
405 int numInViewGroup = 0;
406 int extraEntries = mCollapsedEntriesCount - mEntryViews.size();
407 for (int i = 0; i < mEntryViews.size() && numInViewGroup < mCollapsedEntriesCount;
408 i++) {
409 List<View> entryViewList = mEntryViews.get(i);
Paul Soulos97ed5012014-07-28 16:27:12 -0700410 if (i > 0) {
Paul Souloscc5ec222014-08-25 12:02:26 -0700411 View separator;
412 if (mSeparators.size() <= i - 1) {
413 separator = generateSeparator(entryViewList.get(0));
414 mSeparators.add(separator);
415 } else {
416 separator = mSeparators.get(i - 1);
417 }
Brian Attwell245d3d22015-01-21 09:50:08 -0800418 viewsToDisplay.add(separator);
Paul Soulos97ed5012014-07-28 16:27:12 -0700419 }
Brian Attwell245d3d22015-01-21 09:50:08 -0800420 viewsToDisplay.add(entryViewList.get(0));
Paul Soulos691dd8f2014-08-13 16:10:43 -0700421 numInViewGroup++;
Brian Attwell05287bf2015-02-25 22:24:04 -0800422
Paul Soulos691dd8f2014-08-13 16:10:43 -0700423 // Insert entries in this list to hit mCollapsedEntriesCount.
yaolu139a03b2016-09-02 17:44:10 -0700424 for (int j = 1; j < entryViewList.size() && numInViewGroup < mCollapsedEntriesCount
425 && extraEntries > 0; j++) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800426 viewsToDisplay.add(entryViewList.get(j));
Paul Soulos691dd8f2014-08-13 16:10:43 -0700427 numInViewGroup++;
428 extraEntries--;
429 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700430 }
431 }
432
Brian Attwell245d3d22015-01-21 09:50:08 -0800433 formatEntryIfFirst(viewsToDisplay);
434 return viewsToDisplay;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700435 }
436
Brian Attwell245d3d22015-01-21 09:50:08 -0800437 private void formatEntryIfFirst(List<View> entriesViewGroup) {
Paul Soulos13217362014-08-14 12:27:08 -0700438 // If no title and the first entry in the group, add extra padding
439 if (TextUtils.isEmpty(mTitleTextView.getText()) &&
Brian Attwell245d3d22015-01-21 09:50:08 -0800440 entriesViewGroup.size() > 0) {
441 final View entry = entriesViewGroup.get(0);
yaolu7c6c48a2016-08-15 16:32:05 -0700442 entry.setPaddingRelative(entry.getPaddingStart(),
Paul Soulos568d5d82014-08-20 11:11:20 -0700443 getResources().getDimensionPixelSize(
444 R.dimen.expanding_entry_card_item_padding_top) +
445 getResources().getDimensionPixelSize(
Paul Soulos13217362014-08-14 12:27:08 -0700446 R.dimen.expanding_entry_card_null_title_top_extra_padding),
yaolu7c6c48a2016-08-15 16:32:05 -0700447 entry.getPaddingEnd(),
Paul Soulos13217362014-08-14 12:27:08 -0700448 entry.getPaddingBottom());
449 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700450 }
451
Paul Souloscc5ec222014-08-25 12:02:26 -0700452 private View generateSeparator(View entry) {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700453 View separator = new View(getContext());
Paul Soulosac9b3162014-07-30 16:45:01 -0700454 Resources res = getResources();
455
456 separator.setBackgroundColor(res.getColor(
Brian Attwelldaa20582014-11-25 20:45:44 -0800457 R.color.divider_line_color_light));
Paul Soulosac9b3162014-07-30 16:45:01 -0700458 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
Brian Attwell05287bf2015-02-25 22:24:04 -0800459 ViewGroup.LayoutParams.MATCH_PARENT, mDividerLineHeightPixels);
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700460 // The separator is aligned with the text in the entry. This is offset by a default
461 // margin. If there is an icon present, the icon's width and margin are added
Paul Soulosac9b3162014-07-30 16:45:01 -0700462 int marginStart = res.getDimensionPixelSize(
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700463 R.dimen.expanding_entry_card_item_padding_start);
464 ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon);
Paul Soulos97ed5012014-07-28 16:27:12 -0700465 if (entryIcon.getVisibility() == View.VISIBLE) {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700466 int imageWidthAndMargin =
Paul Soulosac9b3162014-07-30 16:45:01 -0700467 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_icon_width) +
468 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_image_spacing);
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700469 marginStart += imageWidthAndMargin;
470 }
Paul Soulos97ed5012014-07-28 16:27:12 -0700471 layoutParams.setMarginStart(marginStart);
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700472 separator.setLayoutParams(layoutParams);
Paul Souloscc5ec222014-08-25 12:02:26 -0700473 return separator;
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700474 }
475
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700476 private CharSequence getExpandButtonText() {
yaolu139a03b2016-09-02 17:44:10 -0700477 // Default to "See more".
478 return getResources().getText(R.string.expanding_entry_card_view_see_more);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700479 }
480
481 private CharSequence getCollapseButtonText() {
yaolu139a03b2016-09-02 17:44:10 -0700482 // Default to "See less".
483 return getResources().getText(R.string.expanding_entry_card_view_see_less);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700484 }
485
Brian Attwell0d49d812014-06-06 18:01:01 -0700486 /**
Paul Soulos60e51082014-07-10 12:33:04 -0700487 * Inflates the initial entries to be shown.
Brian Attwell0d49d812014-06-06 18:01:01 -0700488 */
Paul Soulos60e51082014-07-10 12:33:04 -0700489 private void inflateInitialEntries(LayoutInflater layoutInflater) {
490 // If the number of collapsed entries equals total entries, inflate all
491 if (mCollapsedEntriesCount == mNumEntries) {
492 inflateAllEntries(layoutInflater);
493 } else {
494 // Otherwise inflate the top entry from each list
Paul Soulos691dd8f2014-08-13 16:10:43 -0700495 // extraEntries is used to add extra entries until mCollapsedEntriesCount is reached.
496 int numInflated = 0;
497 int extraEntries = mCollapsedEntriesCount - mEntries.size();
498 for (int i = 0; i < mEntries.size() && numInflated < mCollapsedEntriesCount; i++) {
499 List<Entry> entryList = mEntries.get(i);
500 List<View> entryViewList = mEntryViews.get(i);
501
502 entryViewList.add(createEntryView(layoutInflater, entryList.get(0),
Paul Soulos97ed5012014-07-28 16:27:12 -0700503 /* showIcon = */ View.VISIBLE));
Paul Soulos691dd8f2014-08-13 16:10:43 -0700504 numInflated++;
Brian Attwell05287bf2015-02-25 22:24:04 -0800505
Paul Soulos691dd8f2014-08-13 16:10:43 -0700506 // Inflate entries in this list to hit mCollapsedEntriesCount.
yaolu139a03b2016-09-02 17:44:10 -0700507 for (int j = 1; j < entryList.size() && numInflated < mCollapsedEntriesCount
Brian Attwell05287bf2015-02-25 22:24:04 -0800508 && extraEntries > 0; j++) {
Paul Soulos691dd8f2014-08-13 16:10:43 -0700509 entryViewList.add(createEntryView(layoutInflater, entryList.get(j),
Paul Soulos43e0dea2014-08-15 16:40:23 -0700510 /* showIcon = */ View.INVISIBLE));
Paul Soulos691dd8f2014-08-13 16:10:43 -0700511 numInflated++;
512 extraEntries--;
513 }
Paul Soulos60e51082014-07-10 12:33:04 -0700514 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700515 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700516 }
517
Paul Soulos60e51082014-07-10 12:33:04 -0700518 /**
519 * Inflates all entries.
520 */
521 private void inflateAllEntries(LayoutInflater layoutInflater) {
522 if (mAllEntriesInflated) {
523 return;
524 }
525 for (int i = 0; i < mEntries.size(); i++) {
526 List<Entry> entryList = mEntries.get(i);
527 List<View> viewList = mEntryViews.get(i);
528 for (int j = viewList.size(); j < entryList.size(); j++) {
Paul Soulos97ed5012014-07-28 16:27:12 -0700529 final int iconVisibility;
530 final Entry entry = entryList.get(j);
531 // If the entry does not have an icon, mark gone. Else if it has an icon, show
532 // for the first Entry in the list only
533 if (entry.getIcon() == null) {
534 iconVisibility = View.GONE;
535 } else if (j == 0) {
536 iconVisibility = View.VISIBLE;
537 } else {
538 iconVisibility = View.INVISIBLE;
539 }
540 viewList.add(createEntryView(layoutInflater, entry, iconVisibility));
Paul Soulos60e51082014-07-10 12:33:04 -0700541 }
542 }
543 mAllEntriesInflated = true;
544 }
545
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700546 public void setColorAndFilter(int color, ColorFilter colorFilter) {
547 mThemeColor = color;
548 mThemeColorFilter = colorFilter;
549 applyColor();
550 }
551
Brian Attwell60953692014-07-11 17:18:46 -0700552 public void setEntryHeaderColor(int color) {
553 if (mEntries != null) {
554 for (List<View> entryList : mEntryViews) {
555 for (View entryView : entryList) {
556 TextView header = (TextView) entryView.findViewById(R.id.header);
557 if (header != null) {
558 header.setTextColor(color);
559 }
560 }
561 }
562 }
563 }
564
guanxiongliu04a19362016-05-18 14:38:51 -0700565 public void setEntrySubHeaderColor(int color) {
566 if (mEntries != null) {
567 for (List<View> entryList : mEntryViews) {
568 for (View entryView : entryList) {
569 final TextView subHeader = (TextView) entryView.findViewById(R.id.sub_header);
570 if (subHeader != null) {
571 subHeader.setTextColor(color);
572 }
573 }
574 }
575 }
576 }
577
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700578 /**
579 * The ColorFilter is passed in along with the color so that a new one only needs to be created
580 * once for the entire activity.
581 * 1. Title
582 * 2. Entry icons
583 * 3. Expand/Collapse Text
584 * 4. Expand/Collapse Button
585 */
586 public void applyColor() {
587 if (mThemeColor != 0 && mThemeColorFilter != null) {
588 // Title
589 if (mTitleTextView != null) {
590 mTitleTextView.setTextColor(mThemeColor);
591 }
592
593 // Entry icons
594 if (mEntries != null) {
Paul Soulos60e51082014-07-10 12:33:04 -0700595 for (List<Entry> entryList : mEntries) {
596 for (Entry entry : entryList) {
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700597 if (entry.shouldApplyColor()) {
598 Drawable icon = entry.getIcon();
599 if (icon != null) {
Brian Attwell0da93002014-10-30 17:25:48 -0700600 icon.mutate();
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700601 icon.setColorFilter(mThemeColorFilter);
602 }
Paul Soulos60e51082014-07-10 12:33:04 -0700603 }
Paul Soulosdd7419d2014-07-15 11:22:13 -0700604 Drawable alternateIcon = entry.getAlternateIcon();
605 if (alternateIcon != null) {
Brian Attwell0da93002014-10-30 17:25:48 -0700606 alternateIcon.mutate();
Paul Soulosdd7419d2014-07-15 11:22:13 -0700607 alternateIcon.setColorFilter(mThemeColorFilter);
608 }
Paul Soulos48fc9122014-08-26 13:52:36 -0700609 Drawable thirdIcon = entry.getThirdIcon();
610 if (thirdIcon != null) {
Brian Attwell0da93002014-10-30 17:25:48 -0700611 thirdIcon.mutate();
Paul Soulos48fc9122014-08-26 13:52:36 -0700612 thirdIcon.setColorFilter(mThemeColorFilter);
613 }
Paul Souloseb64a4b2014-07-07 17:03:27 -0700614 }
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700615 }
616 }
617
618 // Expand/Collapse
619 mExpandCollapseTextView.setTextColor(mThemeColor);
Paul Soulosc205cf12014-08-04 14:35:56 -0700620 mExpandCollapseArrow.setColorFilter(mThemeColorFilter);
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700621 }
622 }
623
Paul Soulos2a4207f2014-07-31 17:09:05 -0700624 private View createEntryView(LayoutInflater layoutInflater, final Entry entry,
625 int iconVisibility) {
626 final EntryView view = (EntryView) layoutInflater.inflate(
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700627 R.layout.expanding_entry_card_item, this, false);
628
Paul Soulos2a4207f2014-07-31 17:09:05 -0700629 view.setContextMenuInfo(entry.getEntryContextMenuInfo());
Paul Soulos23e28362014-08-29 14:57:08 -0700630 if (!TextUtils.isEmpty(entry.getPrimaryContentDescription())) {
631 view.setContentDescription(entry.getPrimaryContentDescription());
632 }
Paul Soulosea5e0b72014-07-08 18:09:44 -0700633
Paul Soulosdd7419d2014-07-15 11:22:13 -0700634 final ImageView icon = (ImageView) view.findViewById(R.id.icon);
Paul Soulos97ed5012014-07-28 16:27:12 -0700635 icon.setVisibility(iconVisibility);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700636 if (entry.getIcon() != null) {
637 icon.setImageDrawable(entry.getIcon());
Paul Souloseb64a4b2014-07-07 17:03:27 -0700638 }
Paul Soulosdd7419d2014-07-15 11:22:13 -0700639 final TextView header = (TextView) view.findViewById(R.id.header);
Paul Soulos03ece862014-07-23 12:32:18 -0700640 if (!TextUtils.isEmpty(entry.getHeader())) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700641 header.setText(entry.getHeader());
642 } else {
643 header.setVisibility(View.GONE);
644 }
645
Paul Soulosdd7419d2014-07-15 11:22:13 -0700646 final TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
Paul Soulos03ece862014-07-23 12:32:18 -0700647 if (!TextUtils.isEmpty(entry.getSubHeader())) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700648 subHeader.setText(entry.getSubHeader());
649 } else {
650 subHeader.setVisibility(View.GONE);
651 }
652
Paul Soulosdd7419d2014-07-15 11:22:13 -0700653 final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700654 if (entry.getSubHeaderIcon() != null) {
655 subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon());
656 } else {
657 subHeaderIcon.setVisibility(View.GONE);
658 }
659
Paul Soulosdd7419d2014-07-15 11:22:13 -0700660 final TextView text = (TextView) view.findViewById(R.id.text);
Paul Soulos03ece862014-07-23 12:32:18 -0700661 if (!TextUtils.isEmpty(entry.getText())) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700662 text.setText(entry.getText());
663 } else {
664 text.setVisibility(View.GONE);
665 }
666
Paul Soulosdd7419d2014-07-15 11:22:13 -0700667 final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700668 if (entry.getTextIcon() != null) {
669 textIcon.setImageDrawable(entry.getTextIcon());
670 } else {
671 textIcon.setVisibility(View.GONE);
672 }
673
674 if (entry.getIntent() != null) {
Paul Soulos7d22b942014-07-08 16:58:15 -0700675 view.setOnClickListener(mOnClickListener);
Paul Soulos2ed2a732014-08-12 11:58:39 -0700676 view.setTag(new EntryTag(entry.getId(), entry.getIntent()));
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700677 }
678
Paul Soulosa42ef762014-08-20 10:26:10 -0700679 if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) {
680 // Remove the click effect
681 view.setBackground(null);
682 }
683
Paul Soulosac9b3162014-07-30 16:45:01 -0700684 // If only the header is visible, add a top margin to match icon's top margin.
685 // Also increase the space below the header for visual comfort.
Paul Soulos4b943552014-07-23 14:49:52 -0700686 if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE &&
687 text.getVisibility() == View.GONE) {
688 RelativeLayout.LayoutParams headerLayoutParams =
689 (RelativeLayout.LayoutParams) header.getLayoutParams();
690 headerLayoutParams.topMargin = (int) (getResources().getDimension(
Paul Soulosac9b3162014-07-30 16:45:01 -0700691 R.dimen.expanding_entry_card_item_header_only_margin_top));
692 headerLayoutParams.bottomMargin += (int) (getResources().getDimension(
693 R.dimen.expanding_entry_card_item_header_only_margin_bottom));
Paul Soulos4b943552014-07-23 14:49:52 -0700694 header.setLayoutParams(headerLayoutParams);
695 }
696
Paul Soulosedd44f02014-08-19 17:07:03 -0700697 // Adjust the top padding size for entries with an invisible icon. The padding depends on
698 // if there is a sub header or text section
699 if (iconVisibility == View.INVISIBLE &&
700 (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) {
701 view.setPaddingRelative(view.getPaddingStart(),
702 getResources().getDimensionPixelSize(
703 R.dimen.expanding_entry_card_item_no_icon_margin_top),
704 view.getPaddingEnd(),
705 view.getPaddingBottom());
706 } else if (iconVisibility == View.INVISIBLE && TextUtils.isEmpty(entry.getSubHeader())
707 && TextUtils.isEmpty(entry.getText())) {
Paul Soulosac9b3162014-07-30 16:45:01 -0700708 view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(),
709 view.getPaddingBottom());
710 }
711
Paul Soulos48fc9122014-08-26 13:52:36 -0700712 final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate);
713 final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon);
714
715 if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) {
716 alternateIcon.setImageDrawable(entry.getAlternateIcon());
717 alternateIcon.setOnClickListener(mOnClickListener);
718 alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent()));
719 alternateIcon.setVisibility(View.VISIBLE);
720 alternateIcon.setContentDescription(entry.getAlternateContentDescription());
721 }
722
Tyler Gunn5f87e922015-08-05 14:24:52 -0700723 if (entry.getThirdIcon() != null && entry.getThirdAction() != Entry.ACTION_NONE) {
Paul Soulos48fc9122014-08-26 13:52:36 -0700724 thirdIcon.setImageDrawable(entry.getThirdIcon());
Tyler Gunn5f87e922015-08-05 14:24:52 -0700725 if (entry.getThirdAction() == Entry.ACTION_INTENT) {
726 thirdIcon.setOnClickListener(mOnClickListener);
727 thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent()));
728 } else if (entry.getThirdAction() == Entry.ACTION_CALL_WITH_SUBJECT) {
729 thirdIcon.setOnClickListener(new View.OnClickListener() {
730 @Override
731 public void onClick(View v) {
732 Object tag = v.getTag();
733 if (!(tag instanceof Bundle)) {
734 return;
735 }
736
737 Context context = getContext();
738 if (context instanceof Activity) {
739 CallSubjectDialog.start((Activity) context, entry.getThirdExtras());
740 }
741 }
742 });
743 thirdIcon.setTag(entry.getThirdExtras());
744 }
Paul Soulos48fc9122014-08-26 13:52:36 -0700745 thirdIcon.setVisibility(View.VISIBLE);
746 thirdIcon.setContentDescription(entry.getThirdContentDescription());
747 }
748
749 // Set a custom touch listener for expanding the extra icon touch areas
750 view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon));
Paul Soulos2a4207f2014-07-31 17:09:05 -0700751 view.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
752
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700753 return view;
754 }
755
Paul Soulosc205cf12014-08-04 14:35:56 -0700756 private void updateExpandCollapseButton(CharSequence buttonText, long duration) {
757 if (mIsExpanded) {
758 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
759 "rotation", 180);
760 animator.setDuration(duration);
761 animator.start();
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700762 } else {
Paul Soulosc205cf12014-08-04 14:35:56 -0700763 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
764 "rotation", 0);
765 animator.setDuration(duration);
766 animator.start();
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700767 }
768 mExpandCollapseTextView.setText(buttonText);
769 }
770
771 private void expand() {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700772 ChangeBounds boundsTransition = new ChangeBounds();
773 boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
774
775 Fade fadeIn = new Fade(Fade.IN);
776 fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN);
777 fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN);
778
779 TransitionSet transitionSet = new TransitionSet();
780 transitionSet.addTransition(boundsTransition);
781 transitionSet.addTransition(fadeIn);
782
Paul Soulos4cd9ed62014-09-09 16:37:07 -0700783 transitionSet.excludeTarget(R.id.text, /* exclude = */ true);
784
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700785 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
786 this : mAnimationViewGroup;
787
788 transitionSet.addListener(new TransitionListener() {
789 @Override
790 public void onTransitionStart(Transition transition) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800791 mListener.onExpand();
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700792 }
793
794 @Override
795 public void onTransitionEnd(Transition transition) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800796 mListener.onExpandDone();
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700797 }
798
799 @Override
800 public void onTransitionCancel(Transition transition) {
801 }
802
803 @Override
804 public void onTransitionPause(Transition transition) {
805 }
806
807 @Override
808 public void onTransitionResume(Transition transition) {
809 }
810 });
811
812 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700813
814 mIsExpanded = true;
Brian Attwell0d49d812014-06-06 18:01:01 -0700815 // In order to insert new entries, we may need to inflate them for the first time
Paul Soulos60e51082014-07-10 12:33:04 -0700816 inflateAllEntries(LayoutInflater.from(getContext()));
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700817 insertEntriesIntoViewGroup();
Paul Soulosc205cf12014-08-04 14:35:56 -0700818 updateExpandCollapseButton(getCollapseButtonText(),
819 DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700820 }
821
822 private void collapse() {
Brian Attwell245d3d22015-01-21 09:50:08 -0800823 final List<View> views = calculateEntriesToRemoveDuringCollapse();
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700824
Brian Attwell245d3d22015-01-21 09:50:08 -0800825 // This animation requires layout changes, unlike the expand() animation: the action bar
826 // might get scrolled open in order to fill empty space. As a result, we can't use
827 // ChangeBounds here. Instead manually animate view height and alpha. This isn't as
828 // efficient as the bounds and translation changes performed by ChangeBounds. Nonetheless, a
829 // reasonable frame-rate is achieved collapsing a dozen elements on a user Svelte N4. So the
830 // performance hit doesn't justify writing a less maintainable animation.
831 final AnimatorSet set = new AnimatorSet();
832 final List<Animator> animators = new ArrayList<Animator>(views.size());
833 int totalSizeChange = 0;
834 for (View viewToRemove : views) {
835 final ObjectAnimator animator = ObjectAnimator.ofObject(viewToRemove,
836 VIEW_LAYOUT_HEIGHT_PROPERTY, null, viewToRemove.getHeight(), 0);
837 totalSizeChange += viewToRemove.getHeight();
838 animator.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
839 animators.add(animator);
840 viewToRemove.animate().alpha(0).setDuration(DURATION_COLLAPSE_ANIMATION_FADE_OUT);
841 }
842 set.playTogether(animators);
843 set.start();
844 set.addListener(new AnimatorListener() {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700845 @Override
Brian Attwell245d3d22015-01-21 09:50:08 -0800846 public void onAnimationStart(Animator animation) {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700847 }
848
849 @Override
Brian Attwell245d3d22015-01-21 09:50:08 -0800850 public void onAnimationEnd(Animator animation) {
851 // Now that the views have been animated away, actually remove them from the view
852 // hierarchy. Reset their appearance so that they look appropriate when they
Brian Attwell05287bf2015-02-25 22:24:04 -0800853 // get added back later.
Brian Attwell245d3d22015-01-21 09:50:08 -0800854 insertEntriesIntoViewGroup();
855 for (View view : views) {
Brian Attwell05287bf2015-02-25 22:24:04 -0800856 if (view instanceof EntryView) {
857 VIEW_LAYOUT_HEIGHT_PROPERTY.set(view, LayoutParams.WRAP_CONTENT);
858 } else {
859 VIEW_LAYOUT_HEIGHT_PROPERTY.set(view, mDividerLineHeightPixels);
860 }
Brian Attwell245d3d22015-01-21 09:50:08 -0800861 view.animate().cancel();
862 view.setAlpha(1);
863 }
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700864 }
865
866 @Override
Brian Attwell245d3d22015-01-21 09:50:08 -0800867 public void onAnimationCancel(Animator animation) {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700868 }
869
870 @Override
Brian Attwell245d3d22015-01-21 09:50:08 -0800871 public void onAnimationRepeat(Animator animation) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700872 }
873 });
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700874
Brian Attwell245d3d22015-01-21 09:50:08 -0800875 mListener.onCollapse(totalSizeChange);
876 mIsExpanded = false;
877 updateExpandCollapseButton(getExpandButtonText(),
878 DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700879 }
880
881 /**
882 * Returns whether the view is currently in its expanded state.
883 */
884 public boolean isExpanded() {
885 return mIsExpanded;
886 }
887
888 /**
889 * Sets the title text of this ExpandingEntryCardView.
Paul Soulos2fa69082014-07-15 12:02:30 -0700890 * @param title The title to set. A null title will result in the title being removed.
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700891 */
892 public void setTitle(String title) {
893 if (mTitleTextView == null) {
894 Log.e(TAG, "mTitleTextView is null");
895 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700896 mTitleTextView.setText(title);
Paul Soulos13217362014-08-14 12:27:08 -0700897 mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
898 findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ?
899 View.GONE : View.VISIBLE);
900 // If the title is set after children have been added, reset the top entry's padding to
901 // the default. Else if the title is cleared after children have been added, set
902 // the extra top padding
903 if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
904 View firstEntry = mEntriesViewGroup.getChildAt(0);
905 firstEntry.setPadding(firstEntry.getPaddingLeft(),
906 getResources().getDimensionPixelSize(
907 R.dimen.expanding_entry_card_item_padding_top),
908 firstEntry.getPaddingRight(),
909 firstEntry.getPaddingBottom());
910 } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
911 View firstEntry = mEntriesViewGroup.getChildAt(0);
912 firstEntry.setPadding(firstEntry.getPaddingLeft(),
913 getResources().getDimensionPixelSize(
914 R.dimen.expanding_entry_card_item_padding_top) +
915 getResources().getDimensionPixelSize(
916 R.dimen.expanding_entry_card_null_title_top_extra_padding),
917 firstEntry.getPaddingRight(),
Brian Attwell88972872015-01-22 14:53:27 -0800918 firstEntry.getPaddingBottom());
Paul Soulos13217362014-08-14 12:27:08 -0700919 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700920 }
Paul Souloseb64a4b2014-07-07 17:03:27 -0700921
922 public boolean shouldShow() {
923 return mEntries != null && mEntries.size() > 0;
924 }
Paul Soulos2a4207f2014-07-31 17:09:05 -0700925
926 public static final class EntryView extends RelativeLayout {
927 private EntryContextMenuInfo mEntryContextMenuInfo;
928
929 public EntryView(Context context) {
930 super(context);
931 }
932
933 public EntryView(Context context, AttributeSet attrs) {
934 super(context, attrs);
935 }
936
937 public void setContextMenuInfo(EntryContextMenuInfo info) {
938 mEntryContextMenuInfo = info;
939 }
940
941 @Override
942 protected ContextMenuInfo getContextMenuInfo() {
943 return mEntryContextMenuInfo;
944 }
945 }
946
947 public static final class EntryContextMenuInfo implements ContextMenuInfo {
948 private final String mCopyText;
949 private final String mCopyLabel;
Paul Soulos97f27802014-09-08 13:55:45 -0700950 private final String mMimeType;
951 private final long mId;
952 private final boolean mIsSuperPrimary;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700953
Paul Soulos97f27802014-09-08 13:55:45 -0700954 public EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id,
955 boolean isSuperPrimary) {
Paul Soulos2a4207f2014-07-31 17:09:05 -0700956 mCopyText = copyText;
957 mCopyLabel = copyLabel;
Paul Soulos97f27802014-09-08 13:55:45 -0700958 mMimeType = mimeType;
959 mId = id;
960 mIsSuperPrimary = isSuperPrimary;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700961 }
962
963 public String getCopyText() {
964 return mCopyText;
965 }
966
967 public String getCopyLabel() {
968 return mCopyLabel;
969 }
Paul Soulos97f27802014-09-08 13:55:45 -0700970
971 public String getMimeType() {
972 return mMimeType;
973 }
974
975 public long getId() {
976 return mId;
977 }
978
979 public boolean isSuperPrimary() {
980 return mIsSuperPrimary;
981 }
Paul Soulos2a4207f2014-07-31 17:09:05 -0700982 }
Paul Soulos2ed2a732014-08-12 11:58:39 -0700983
984 static final class EntryTag {
985 private final int mId;
986 private final Intent mIntent;
987
988 public EntryTag(int id, Intent intent) {
989 mId = id;
990 mIntent = intent;
991 }
992
993 public int getId() {
994 return mId;
995 }
996
997 public Intent getIntent() {
998 return mIntent;
999 }
1000 }
Paul Soulos48fc9122014-08-26 13:52:36 -07001001
1002 /**
1003 * This custom touch listener increases the touch area for the second and third icons, if
1004 * they are present. This is necessary to maintain other properties on an entry view, like
1005 * using a top padding on entry. Based off of {@link android.view.TouchDelegate}
1006 */
1007 private static final class EntryTouchListener implements View.OnTouchListener {
1008 private final View mEntry;
1009 private final ImageView mAlternateIcon;
1010 private final ImageView mThirdIcon;
1011 /** mTouchedView locks in a view on touch down */
1012 private View mTouchedView;
1013 /** mSlop adds some space to account for touches that are just outside the hit area */
1014 private int mSlop;
1015
1016 public EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon) {
1017 mEntry = entry;
1018 mAlternateIcon = alternateIcon;
1019 mThirdIcon = thirdIcon;
1020 mSlop = ViewConfiguration.get(entry.getContext()).getScaledTouchSlop();
1021 }
1022
1023 @Override
1024 public boolean onTouch(View v, MotionEvent event) {
1025 View touchedView = mTouchedView;
1026 boolean sendToTouched = false;
1027 boolean hit = true;
1028 boolean handled = false;
1029
1030 switch (event.getAction()) {
1031 case MotionEvent.ACTION_DOWN:
1032 if (hitThirdIcon(event)) {
1033 mTouchedView = mThirdIcon;
1034 sendToTouched = true;
1035 } else if (hitAlternateIcon(event)) {
1036 mTouchedView = mAlternateIcon;
1037 sendToTouched = true;
1038 } else {
1039 mTouchedView = mEntry;
1040 sendToTouched = false;
1041 }
1042 touchedView = mTouchedView;
1043 break;
1044 case MotionEvent.ACTION_UP:
1045 case MotionEvent.ACTION_MOVE:
1046 sendToTouched = mTouchedView != null && mTouchedView != mEntry;
1047 if (sendToTouched) {
1048 final Rect slopBounds = new Rect();
1049 touchedView.getHitRect(slopBounds);
1050 slopBounds.inset(-mSlop, -mSlop);
1051 if (!slopBounds.contains((int) event.getX(), (int) event.getY())) {
1052 hit = false;
1053 }
1054 }
1055 break;
1056 case MotionEvent.ACTION_CANCEL:
1057 sendToTouched = mTouchedView != null && mTouchedView != mEntry;
1058 mTouchedView = null;
1059 break;
1060 }
1061 if (sendToTouched) {
1062 if (hit) {
1063 event.setLocation(touchedView.getWidth() / 2, touchedView.getHeight() / 2);
1064 } else {
1065 // Offset event coordinates to be outside the target view (in case it does
1066 // something like tracking pressed state)
1067 event.setLocation(-(mSlop * 2), -(mSlop * 2));
1068 }
1069 handled = touchedView.dispatchTouchEvent(event);
1070 }
1071 return handled;
1072 }
1073
1074 private boolean hitThirdIcon(MotionEvent event) {
Brian Attwellf1402272014-12-16 16:00:08 -08001075 if (mEntry.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
Paul Soulos48fc9122014-08-26 13:52:36 -07001076 return mThirdIcon.getVisibility() == View.VISIBLE &&
1077 event.getX() < mThirdIcon.getRight();
1078 } else {
1079 return mThirdIcon.getVisibility() == View.VISIBLE &&
1080 event.getX() > mThirdIcon.getLeft();
1081 }
1082 }
1083
1084 /**
1085 * Should be used after checking if third icon was hit
1086 */
1087 private boolean hitAlternateIcon(MotionEvent event) {
1088 // LayoutParams used to add the start margin to the touch area
1089 final RelativeLayout.LayoutParams alternateIconParams =
1090 (RelativeLayout.LayoutParams) mAlternateIcon.getLayoutParams();
Brian Attwellf1402272014-12-16 16:00:08 -08001091 if (mEntry.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
Paul Soulos48fc9122014-08-26 13:52:36 -07001092 return mAlternateIcon.getVisibility() == View.VISIBLE &&
1093 event.getX() < mAlternateIcon.getRight() + alternateIconParams.rightMargin;
1094 } else {
1095 return mAlternateIcon.getVisibility() == View.VISIBLE &&
1096 event.getX() > mAlternateIcon.getLeft() - alternateIconParams.leftMargin;
1097 }
1098 }
1099 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -07001100}