blob: d7efaf473416b4d32bf77cb7c10f52453495b8bf [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;
Brian Attwell245d3d22015-01-21 09:50:08 -080050import android.widget.LinearLayout.LayoutParams;
Paul Soulos4b943552014-07-23 14:49:52 -070051import android.widget.RelativeLayout;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070052import android.widget.TextView;
53
Paul Soulos0cda9ae2014-07-23 11:27:28 -070054import com.android.contacts.R;
Tyler Gunn5f87e922015-08-05 14:24:52 -070055import com.android.contacts.common.dialog.CallSubjectDialog;
Paul Soulos0cda9ae2014-07-23 11:27:28 -070056
Paul Soulos2d48b5a2014-05-29 13:56:25 -070057import java.util.ArrayList;
58import java.util.List;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070059
60/**
61 * Display entries in a LinearLayout that can be expanded to show all entries.
62 */
Paul Soulosac9b3162014-07-30 16:45:01 -070063public class ExpandingEntryCardView extends CardView {
Paul Soulos2d48b5a2014-05-29 13:56:25 -070064
65 private static final String TAG = "ExpandingEntryCardView";
Paul Soulos0cda9ae2014-07-23 11:27:28 -070066 private static final int DURATION_EXPAND_ANIMATION_FADE_IN = 200;
Brian Attwell245d3d22015-01-21 09:50:08 -080067 private static final int DURATION_COLLAPSE_ANIMATION_FADE_OUT = 75;
Paul Soulos0cda9ae2014-07-23 11:27:28 -070068 private static final int DELAY_EXPAND_ANIMATION_FADE_IN = 100;
69
70 public static final int DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS = 300;
71 public static final int DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS = 300;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070072
Brian Attwell245d3d22015-01-21 09:50:08 -080073 private static final Property<View, Integer> VIEW_LAYOUT_HEIGHT_PROPERTY =
74 new Property<View, Integer>(Integer.class, "height") {
75 @Override
76 public void set(View view, Integer height) {
77 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
78 view.getLayoutParams();
79 params.height = height;
80 view.setLayoutParams(params);
81 }
82
83 @Override
84 public Integer get(View view) {
85 return view.getLayoutParams().height;
86 }
87 };
88
Paul Soulos2d48b5a2014-05-29 13:56:25 -070089 /**
90 * Entry data.
91 */
92 public static final class Entry {
Tyler Gunn5f87e922015-08-05 14:24:52 -070093 // No action when clicking a button is specified.
94 public static final int ACTION_NONE = 1;
95 // Button action is an intent.
96 public static final int ACTION_INTENT = 2;
97 // Button action will open the call with subject dialog.
98 public static final int ACTION_CALL_WITH_SUBJECT = 3;
Paul Soulos2d48b5a2014-05-29 13:56:25 -070099
Paul Soulos2ed2a732014-08-12 11:58:39 -0700100 private final int mId;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700101 private final Drawable mIcon;
102 private final String mHeader;
103 private final String mSubHeader;
104 private final Drawable mSubHeaderIcon;
105 private final String mText;
106 private final Drawable mTextIcon;
Walter Jang7ce53522014-10-29 13:26:43 -0700107 private Spannable mPrimaryContentDescription;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700108 private final Intent mIntent;
Paul Soulosdd7419d2014-07-15 11:22:13 -0700109 private final Drawable mAlternateIcon;
110 private final Intent mAlternateIntent;
Wenyi Wang5da55ff2015-11-19 13:22:40 -0800111 private Spannable mAlternateContentDescription;
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700112 private final boolean mShouldApplyColor;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700113 private final boolean mIsEditable;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700114 private final EntryContextMenuInfo mEntryContextMenuInfo;
Paul Soulos48fc9122014-08-26 13:52:36 -0700115 private final Drawable mThirdIcon;
116 private final Intent mThirdIntent;
117 private final String mThirdContentDescription;
Paul Soulos48290be2014-09-08 13:44:51 -0700118 private final int mIconResourceId;
Tyler Gunn5f87e922015-08-05 14:24:52 -0700119 private final int mThirdAction;
120 private final Bundle mThirdExtras;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700121
Paul Soulos2ed2a732014-08-12 11:58:39 -0700122 public Entry(int id, Drawable mainIcon, String header, String subHeader,
Paul Soulos23e28362014-08-29 14:57:08 -0700123 Drawable subHeaderIcon, String text, Drawable textIcon,
Walter Jang7ce53522014-10-29 13:26:43 -0700124 Spannable primaryContentDescription, Intent intent,
Wenyi Wang5da55ff2015-11-19 13:22:40 -0800125 Drawable alternateIcon, Intent alternateIntent,
126 Spannable alternateContentDescription, boolean shouldApplyColor, boolean isEditable,
Paul Soulos48fc9122014-08-26 13:52:36 -0700127 EntryContextMenuInfo entryContextMenuInfo, Drawable thirdIcon, Intent thirdIntent,
Tyler Gunn5f87e922015-08-05 14:24:52 -0700128 String thirdContentDescription, int thirdAction, Bundle thirdExtras,
129 int iconResourceId) {
Paul Soulos2ed2a732014-08-12 11:58:39 -0700130 mId = id;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700131 mIcon = mainIcon;
132 mHeader = header;
133 mSubHeader = subHeader;
134 mSubHeaderIcon = subHeaderIcon;
135 mText = text;
136 mTextIcon = textIcon;
Paul Soulos23e28362014-08-29 14:57:08 -0700137 mPrimaryContentDescription = primaryContentDescription;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700138 mIntent = intent;
Paul Soulosdd7419d2014-07-15 11:22:13 -0700139 mAlternateIcon = alternateIcon;
140 mAlternateIntent = alternateIntent;
141 mAlternateContentDescription = alternateContentDescription;
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700142 mShouldApplyColor = shouldApplyColor;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700143 mIsEditable = isEditable;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700144 mEntryContextMenuInfo = entryContextMenuInfo;
Paul Soulos48fc9122014-08-26 13:52:36 -0700145 mThirdIcon = thirdIcon;
146 mThirdIntent = thirdIntent;
147 mThirdContentDescription = thirdContentDescription;
Tyler Gunn5f87e922015-08-05 14:24:52 -0700148 mThirdAction = thirdAction;
149 mThirdExtras = thirdExtras;
Paul Soulos48290be2014-09-08 13:44:51 -0700150 mIconResourceId = iconResourceId;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700151 }
152
153 Drawable getIcon() {
154 return mIcon;
155 }
156
157 String getHeader() {
158 return mHeader;
159 }
160
161 String getSubHeader() {
162 return mSubHeader;
163 }
164
165 Drawable getSubHeaderIcon() {
166 return mSubHeaderIcon;
167 }
168
169 public String getText() {
170 return mText;
171 }
172
173 Drawable getTextIcon() {
174 return mTextIcon;
175 }
176
Walter Jang7ce53522014-10-29 13:26:43 -0700177 Spannable getPrimaryContentDescription() {
Paul Soulos23e28362014-08-29 14:57:08 -0700178 return mPrimaryContentDescription;
179 }
180
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700181 Intent getIntent() {
182 return mIntent;
183 }
184
Paul Soulosdd7419d2014-07-15 11:22:13 -0700185 Drawable getAlternateIcon() {
186 return mAlternateIcon;
187 }
188
189 Intent getAlternateIntent() {
190 return mAlternateIntent;
191 }
192
Wenyi Wang5da55ff2015-11-19 13:22:40 -0800193 Spannable getAlternateContentDescription() {
Paul Soulosdd7419d2014-07-15 11:22:13 -0700194 return mAlternateContentDescription;
195 }
196
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700197 boolean shouldApplyColor() {
198 return mShouldApplyColor;
199 }
200
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700201 boolean isEditable() {
202 return mIsEditable;
203 }
Paul Soulosea5e0b72014-07-08 18:09:44 -0700204
Paul Soulos2ed2a732014-08-12 11:58:39 -0700205 int getId() {
206 return mId;
Paul Soulosea5e0b72014-07-08 18:09:44 -0700207 }
Paul Soulos2a4207f2014-07-31 17:09:05 -0700208
209 EntryContextMenuInfo getEntryContextMenuInfo() {
210 return mEntryContextMenuInfo;
211 }
Paul Soulos48fc9122014-08-26 13:52:36 -0700212
213 Drawable getThirdIcon() {
214 return mThirdIcon;
215 }
216
217 Intent getThirdIntent() {
218 return mThirdIntent;
219 }
220
221 String getThirdContentDescription() {
222 return mThirdContentDescription;
223 }
Paul Soulos48290be2014-09-08 13:44:51 -0700224
225 int getIconResourceId() {
226 return mIconResourceId;
227 }
Tyler Gunn5f87e922015-08-05 14:24:52 -0700228
229 public int getThirdAction() {
230 return mThirdAction;
231 }
232
233 public Bundle getThirdExtras() {
234 return mThirdExtras;
235 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700236 }
237
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700238 public interface ExpandingEntryCardViewListener {
239 void onCollapse(int heightDelta);
Brian Attwell245d3d22015-01-21 09:50:08 -0800240 void onExpand();
241 void onExpandDone();
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700242 }
243
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700244 private View mExpandCollapseButton;
245 private TextView mExpandCollapseTextView;
246 private TextView mTitleTextView;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700247 private OnClickListener mOnClickListener;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700248 private OnCreateContextMenuListener mOnCreateContextMenuListener;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700249 private boolean mIsExpanded = false;
Paul Soulos691dd8f2014-08-13 16:10:43 -0700250 /**
251 * The max number of entries to show in a collapsed card. If there are less entries passed in,
252 * then they are all shown.
253 */
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700254 private int mCollapsedEntriesCount;
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700255 private ExpandingEntryCardViewListener mListener;
Paul Soulos60e51082014-07-10 12:33:04 -0700256 private List<List<Entry>> mEntries;
257 private int mNumEntries = 0;
258 private boolean mAllEntriesInflated = false;
259 private List<List<View>> mEntryViews;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700260 private LinearLayout mEntriesViewGroup;
Paul Soulosc205cf12014-08-04 14:35:56 -0700261 private final ImageView mExpandCollapseArrow;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700262 private int mThemeColor;
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700263 private ColorFilter mThemeColorFilter;
Paul Soulos89966b42014-07-21 12:38:50 -0700264 private boolean mIsAlwaysExpanded;
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700265 /** The ViewGroup to run the expand/collapse animation on */
266 private ViewGroup mAnimationViewGroup;
Brian Attwell05287bf2015-02-25 22:24:04 -0800267 private final int mDividerLineHeightPixels;
Paul Souloscc5ec222014-08-25 12:02:26 -0700268 /**
269 * List to hold the separators. This saves us from reconstructing every expand/collapse and
270 * provides a smoother animation.
271 */
272 private List<View> mSeparators;
Paul Soulosac9b3162014-07-30 16:45:01 -0700273 private LinearLayout mContainer;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700274
275 private final OnClickListener mExpandCollapseButtonListener = new OnClickListener() {
276 @Override
277 public void onClick(View v) {
278 if (mIsExpanded) {
279 collapse();
280 } else {
281 expand();
282 }
283 }
284 };
285
286 public ExpandingEntryCardView(Context context) {
Brian Attwell0d49d812014-06-06 18:01:01 -0700287 this(context, null);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700288 }
289
290 public ExpandingEntryCardView(Context context, AttributeSet attrs) {
291 super(context, attrs);
292 LayoutInflater inflater = LayoutInflater.from(context);
293 View expandingEntryCardView = inflater.inflate(R.layout.expanding_entry_card_view, this);
294 mEntriesViewGroup = (LinearLayout)
295 expandingEntryCardView.findViewById(R.id.content_area_linear_layout);
296 mTitleTextView = (TextView) expandingEntryCardView.findViewById(R.id.title);
Paul Soulosac9b3162014-07-30 16:45:01 -0700297 mContainer = (LinearLayout) expandingEntryCardView.findViewById(R.id.container);
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700298
299 mExpandCollapseButton = inflater.inflate(
300 R.layout.quickcontact_expanding_entry_card_button, this, false);
301 mExpandCollapseTextView = (TextView) mExpandCollapseButton.findViewById(R.id.text);
Paul Soulosc205cf12014-08-04 14:35:56 -0700302 mExpandCollapseArrow = (ImageView) mExpandCollapseButton.findViewById(R.id.arrow);
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700303 mExpandCollapseButton.setOnClickListener(mExpandCollapseButtonListener);
Brian Attwell05287bf2015-02-25 22:24:04 -0800304 mDividerLineHeightPixels = getResources()
305 .getDimensionPixelSize(R.dimen.divider_line_height);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700306 }
307
308 /**
309 * Sets the Entry list to display.
310 *
311 * @param entries The Entry list to display.
312 */
Paul Soulos60e51082014-07-10 12:33:04 -0700313 public void initialize(List<List<Entry>> entries, int numInitialVisibleEntries,
yaolu139a03b2016-09-02 17:44:10 -0700314 boolean isExpanded, boolean isAlwaysExpanded, ExpandingEntryCardViewListener listener,
315 ViewGroup animationViewGroup) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700316 LayoutInflater layoutInflater = LayoutInflater.from(getContext());
317 mIsExpanded = isExpanded;
Paul Soulos89966b42014-07-21 12:38:50 -0700318 mIsAlwaysExpanded = isAlwaysExpanded;
319 // If isAlwaysExpanded is true, mIsExpanded should be true
320 mIsExpanded |= mIsAlwaysExpanded;
Paul Soulos60e51082014-07-10 12:33:04 -0700321 mEntryViews = new ArrayList<List<View>>(entries.size());
Brian Attwell0d49d812014-06-06 18:01:01 -0700322 mEntries = entries;
Brian Attwell60953692014-07-11 17:18:46 -0700323 mNumEntries = 0;
324 mAllEntriesInflated = false;
Paul Soulos60e51082014-07-10 12:33:04 -0700325 for (List<Entry> entryList : mEntries) {
326 mNumEntries += entryList.size();
327 mEntryViews.add(new ArrayList<View>());
328 }
329 mCollapsedEntriesCount = Math.min(numInitialVisibleEntries, mNumEntries);
Paul Souloscc5ec222014-08-25 12:02:26 -0700330 // We need a separator between each list, but not after the last one
331 if (entries.size() > 1) {
332 mSeparators = new ArrayList<>(entries.size() - 1);
333 }
Brian Attwelle8ce6ee2014-06-27 18:26:32 -0700334 mListener = listener;
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700335 mAnimationViewGroup = animationViewGroup;
Brian Attwell0d49d812014-06-06 18:01:01 -0700336
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700337 if (mIsExpanded) {
Paul Soulosc205cf12014-08-04 14:35:56 -0700338 updateExpandCollapseButton(getCollapseButtonText(), /* duration = */ 0);
Paul Soulos60e51082014-07-10 12:33:04 -0700339 inflateAllEntries(layoutInflater);
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700340 } else {
Paul Soulosc205cf12014-08-04 14:35:56 -0700341 updateExpandCollapseButton(getExpandButtonText(), /* duration = */ 0);
Paul Soulos60e51082014-07-10 12:33:04 -0700342 inflateInitialEntries(layoutInflater);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700343 }
344 insertEntriesIntoViewGroup();
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700345 applyColor();
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700346 }
347
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700348 @Override
349 public void setOnClickListener(OnClickListener listener) {
350 mOnClickListener = listener;
351 }
352
Paul Soulos2a4207f2014-07-31 17:09:05 -0700353 @Override
354 public void setOnCreateContextMenuListener (OnCreateContextMenuListener listener) {
355 mOnCreateContextMenuListener = listener;
356 }
357
Brian Attwell245d3d22015-01-21 09:50:08 -0800358 private List<View> calculateEntriesToRemoveDuringCollapse() {
359 final List<View> viewsToRemove = getViewsToDisplay(true);
360 final List<View> viewsCollapsed = getViewsToDisplay(false);
361 viewsToRemove.removeAll(viewsCollapsed);
362 return viewsToRemove;
363 }
364
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700365 private void insertEntriesIntoViewGroup() {
366 mEntriesViewGroup.removeAllViews();
Paul Soulos60e51082014-07-10 12:33:04 -0700367
Brian Attwell245d3d22015-01-21 09:50:08 -0800368 for (View view : getViewsToDisplay(mIsExpanded)) {
369 mEntriesViewGroup.addView(view);
370 }
371
372 removeView(mExpandCollapseButton);
373 if (mCollapsedEntriesCount < mNumEntries
374 && mExpandCollapseButton.getParent() == null && !mIsAlwaysExpanded) {
375 mContainer.addView(mExpandCollapseButton, -1);
376 }
377 }
378
379 /**
380 * Returns the list of views that should be displayed. This changes depending on whether
381 * the card is expanded or collapsed.
382 */
383 private List<View> getViewsToDisplay(boolean isExpanded) {
384 final List<View> viewsToDisplay = new ArrayList<View>();
385 if (isExpanded) {
Paul Souloscc5ec222014-08-25 12:02:26 -0700386 for (int i = 0; i < mEntryViews.size(); i++) {
387 List<View> viewList = mEntryViews.get(i);
388 if (i > 0) {
389 View separator;
390 if (mSeparators.size() <= i - 1) {
391 separator = generateSeparator(viewList.get(0));
392 mSeparators.add(separator);
393 } else {
394 separator = mSeparators.get(i - 1);
395 }
Brian Attwell245d3d22015-01-21 09:50:08 -0800396 viewsToDisplay.add(separator);
Paul Soulos97ed5012014-07-28 16:27:12 -0700397 }
Paul Soulos60e51082014-07-10 12:33:04 -0700398 for (View view : viewList) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800399 viewsToDisplay.add(view);
Paul Soulos60e51082014-07-10 12:33:04 -0700400 }
401 }
402 } else {
Paul Soulos691dd8f2014-08-13 16:10:43 -0700403 // We want to insert mCollapsedEntriesCount entries into the group. extraEntries is the
404 // number of entries that need to be added that are not the head element of a list
405 // to reach mCollapsedEntriesCount.
406 int numInViewGroup = 0;
407 int extraEntries = mCollapsedEntriesCount - mEntryViews.size();
408 for (int i = 0; i < mEntryViews.size() && numInViewGroup < mCollapsedEntriesCount;
409 i++) {
410 List<View> entryViewList = mEntryViews.get(i);
Paul Soulos97ed5012014-07-28 16:27:12 -0700411 if (i > 0) {
Paul Souloscc5ec222014-08-25 12:02:26 -0700412 View separator;
413 if (mSeparators.size() <= i - 1) {
414 separator = generateSeparator(entryViewList.get(0));
415 mSeparators.add(separator);
416 } else {
417 separator = mSeparators.get(i - 1);
418 }
Brian Attwell245d3d22015-01-21 09:50:08 -0800419 viewsToDisplay.add(separator);
Paul Soulos97ed5012014-07-28 16:27:12 -0700420 }
Brian Attwell245d3d22015-01-21 09:50:08 -0800421 viewsToDisplay.add(entryViewList.get(0));
Paul Soulos691dd8f2014-08-13 16:10:43 -0700422 numInViewGroup++;
Brian Attwell05287bf2015-02-25 22:24:04 -0800423
Paul Soulos691dd8f2014-08-13 16:10:43 -0700424 // Insert entries in this list to hit mCollapsedEntriesCount.
yaolu139a03b2016-09-02 17:44:10 -0700425 for (int j = 1; j < entryViewList.size() && numInViewGroup < mCollapsedEntriesCount
426 && extraEntries > 0; j++) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800427 viewsToDisplay.add(entryViewList.get(j));
Paul Soulos691dd8f2014-08-13 16:10:43 -0700428 numInViewGroup++;
429 extraEntries--;
430 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700431 }
432 }
433
Brian Attwell245d3d22015-01-21 09:50:08 -0800434 formatEntryIfFirst(viewsToDisplay);
435 return viewsToDisplay;
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700436 }
437
Brian Attwell245d3d22015-01-21 09:50:08 -0800438 private void formatEntryIfFirst(List<View> entriesViewGroup) {
Paul Soulos13217362014-08-14 12:27:08 -0700439 // If no title and the first entry in the group, add extra padding
440 if (TextUtils.isEmpty(mTitleTextView.getText()) &&
Brian Attwell245d3d22015-01-21 09:50:08 -0800441 entriesViewGroup.size() > 0) {
442 final View entry = entriesViewGroup.get(0);
yaolu7c6c48a2016-08-15 16:32:05 -0700443 entry.setPaddingRelative(entry.getPaddingStart(),
Paul Soulos568d5d82014-08-20 11:11:20 -0700444 getResources().getDimensionPixelSize(
445 R.dimen.expanding_entry_card_item_padding_top) +
446 getResources().getDimensionPixelSize(
Paul Soulos13217362014-08-14 12:27:08 -0700447 R.dimen.expanding_entry_card_null_title_top_extra_padding),
yaolu7c6c48a2016-08-15 16:32:05 -0700448 entry.getPaddingEnd(),
Paul Soulos13217362014-08-14 12:27:08 -0700449 entry.getPaddingBottom());
450 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700451 }
452
Paul Souloscc5ec222014-08-25 12:02:26 -0700453 private View generateSeparator(View entry) {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700454 View separator = new View(getContext());
Paul Soulosac9b3162014-07-30 16:45:01 -0700455 Resources res = getResources();
456
457 separator.setBackgroundColor(res.getColor(
Brian Attwelldaa20582014-11-25 20:45:44 -0800458 R.color.divider_line_color_light));
Paul Soulosac9b3162014-07-30 16:45:01 -0700459 LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
Brian Attwell05287bf2015-02-25 22:24:04 -0800460 ViewGroup.LayoutParams.MATCH_PARENT, mDividerLineHeightPixels);
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700461 // The separator is aligned with the text in the entry. This is offset by a default
462 // margin. If there is an icon present, the icon's width and margin are added
Paul Soulosac9b3162014-07-30 16:45:01 -0700463 int marginStart = res.getDimensionPixelSize(
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700464 R.dimen.expanding_entry_card_item_padding_start);
465 ImageView entryIcon = (ImageView) entry.findViewById(R.id.icon);
Paul Soulos97ed5012014-07-28 16:27:12 -0700466 if (entryIcon.getVisibility() == View.VISIBLE) {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700467 int imageWidthAndMargin =
Paul Soulosac9b3162014-07-30 16:45:01 -0700468 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_icon_width) +
469 res.getDimensionPixelSize(R.dimen.expanding_entry_card_item_image_spacing);
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700470 marginStart += imageWidthAndMargin;
471 }
Paul Soulos97ed5012014-07-28 16:27:12 -0700472 layoutParams.setMarginStart(marginStart);
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700473 separator.setLayoutParams(layoutParams);
Paul Souloscc5ec222014-08-25 12:02:26 -0700474 return separator;
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700475 }
476
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700477 private CharSequence getExpandButtonText() {
yaolu139a03b2016-09-02 17:44:10 -0700478 // Default to "See more".
479 return getResources().getText(R.string.expanding_entry_card_view_see_more);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700480 }
481
482 private CharSequence getCollapseButtonText() {
yaolu139a03b2016-09-02 17:44:10 -0700483 // Default to "See less".
484 return getResources().getText(R.string.expanding_entry_card_view_see_less);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700485 }
486
Brian Attwell0d49d812014-06-06 18:01:01 -0700487 /**
Paul Soulos60e51082014-07-10 12:33:04 -0700488 * Inflates the initial entries to be shown.
Brian Attwell0d49d812014-06-06 18:01:01 -0700489 */
Paul Soulos60e51082014-07-10 12:33:04 -0700490 private void inflateInitialEntries(LayoutInflater layoutInflater) {
491 // If the number of collapsed entries equals total entries, inflate all
492 if (mCollapsedEntriesCount == mNumEntries) {
493 inflateAllEntries(layoutInflater);
494 } else {
495 // Otherwise inflate the top entry from each list
Paul Soulos691dd8f2014-08-13 16:10:43 -0700496 // extraEntries is used to add extra entries until mCollapsedEntriesCount is reached.
497 int numInflated = 0;
498 int extraEntries = mCollapsedEntriesCount - mEntries.size();
499 for (int i = 0; i < mEntries.size() && numInflated < mCollapsedEntriesCount; i++) {
500 List<Entry> entryList = mEntries.get(i);
501 List<View> entryViewList = mEntryViews.get(i);
502
503 entryViewList.add(createEntryView(layoutInflater, entryList.get(0),
Paul Soulos97ed5012014-07-28 16:27:12 -0700504 /* showIcon = */ View.VISIBLE));
Paul Soulos691dd8f2014-08-13 16:10:43 -0700505 numInflated++;
Brian Attwell05287bf2015-02-25 22:24:04 -0800506
Paul Soulos691dd8f2014-08-13 16:10:43 -0700507 // Inflate entries in this list to hit mCollapsedEntriesCount.
yaolu139a03b2016-09-02 17:44:10 -0700508 for (int j = 1; j < entryList.size() && numInflated < mCollapsedEntriesCount
Brian Attwell05287bf2015-02-25 22:24:04 -0800509 && extraEntries > 0; j++) {
Paul Soulos691dd8f2014-08-13 16:10:43 -0700510 entryViewList.add(createEntryView(layoutInflater, entryList.get(j),
Paul Soulos43e0dea2014-08-15 16:40:23 -0700511 /* showIcon = */ View.INVISIBLE));
Paul Soulos691dd8f2014-08-13 16:10:43 -0700512 numInflated++;
513 extraEntries--;
514 }
Paul Soulos60e51082014-07-10 12:33:04 -0700515 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700516 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700517 }
518
Paul Soulos60e51082014-07-10 12:33:04 -0700519 /**
520 * Inflates all entries.
521 */
522 private void inflateAllEntries(LayoutInflater layoutInflater) {
523 if (mAllEntriesInflated) {
524 return;
525 }
526 for (int i = 0; i < mEntries.size(); i++) {
527 List<Entry> entryList = mEntries.get(i);
528 List<View> viewList = mEntryViews.get(i);
529 for (int j = viewList.size(); j < entryList.size(); j++) {
Paul Soulos97ed5012014-07-28 16:27:12 -0700530 final int iconVisibility;
531 final Entry entry = entryList.get(j);
532 // If the entry does not have an icon, mark gone. Else if it has an icon, show
533 // for the first Entry in the list only
534 if (entry.getIcon() == null) {
535 iconVisibility = View.GONE;
536 } else if (j == 0) {
537 iconVisibility = View.VISIBLE;
538 } else {
539 iconVisibility = View.INVISIBLE;
540 }
541 viewList.add(createEntryView(layoutInflater, entry, iconVisibility));
Paul Soulos60e51082014-07-10 12:33:04 -0700542 }
543 }
544 mAllEntriesInflated = true;
545 }
546
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700547 public void setColorAndFilter(int color, ColorFilter colorFilter) {
548 mThemeColor = color;
549 mThemeColorFilter = colorFilter;
550 applyColor();
551 }
552
Brian Attwell60953692014-07-11 17:18:46 -0700553 public void setEntryHeaderColor(int color) {
554 if (mEntries != null) {
555 for (List<View> entryList : mEntryViews) {
556 for (View entryView : entryList) {
557 TextView header = (TextView) entryView.findViewById(R.id.header);
558 if (header != null) {
559 header.setTextColor(color);
560 }
561 }
562 }
563 }
564 }
565
guanxiongliu04a19362016-05-18 14:38:51 -0700566 public void setEntrySubHeaderColor(int color) {
567 if (mEntries != null) {
568 for (List<View> entryList : mEntryViews) {
569 for (View entryView : entryList) {
570 final TextView subHeader = (TextView) entryView.findViewById(R.id.sub_header);
571 if (subHeader != null) {
572 subHeader.setTextColor(color);
573 }
574 }
575 }
576 }
577 }
578
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700579 /**
580 * The ColorFilter is passed in along with the color so that a new one only needs to be created
581 * once for the entire activity.
582 * 1. Title
583 * 2. Entry icons
584 * 3. Expand/Collapse Text
585 * 4. Expand/Collapse Button
586 */
587 public void applyColor() {
588 if (mThemeColor != 0 && mThemeColorFilter != null) {
589 // Title
590 if (mTitleTextView != null) {
591 mTitleTextView.setTextColor(mThemeColor);
592 }
593
594 // Entry icons
595 if (mEntries != null) {
Paul Soulos60e51082014-07-10 12:33:04 -0700596 for (List<Entry> entryList : mEntries) {
597 for (Entry entry : entryList) {
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700598 if (entry.shouldApplyColor()) {
599 Drawable icon = entry.getIcon();
600 if (icon != null) {
Brian Attwell0da93002014-10-30 17:25:48 -0700601 icon.mutate();
Paul Soulos48ebbaa2014-07-15 13:11:23 -0700602 icon.setColorFilter(mThemeColorFilter);
603 }
Paul Soulos60e51082014-07-10 12:33:04 -0700604 }
Paul Soulosdd7419d2014-07-15 11:22:13 -0700605 Drawable alternateIcon = entry.getAlternateIcon();
606 if (alternateIcon != null) {
Brian Attwell0da93002014-10-30 17:25:48 -0700607 alternateIcon.mutate();
Paul Soulosdd7419d2014-07-15 11:22:13 -0700608 alternateIcon.setColorFilter(mThemeColorFilter);
609 }
Paul Soulos48fc9122014-08-26 13:52:36 -0700610 Drawable thirdIcon = entry.getThirdIcon();
611 if (thirdIcon != null) {
Brian Attwell0da93002014-10-30 17:25:48 -0700612 thirdIcon.mutate();
Paul Soulos48fc9122014-08-26 13:52:36 -0700613 thirdIcon.setColorFilter(mThemeColorFilter);
614 }
Paul Souloseb64a4b2014-07-07 17:03:27 -0700615 }
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700616 }
617 }
618
619 // Expand/Collapse
620 mExpandCollapseTextView.setTextColor(mThemeColor);
Paul Soulosc205cf12014-08-04 14:35:56 -0700621 mExpandCollapseArrow.setColorFilter(mThemeColorFilter);
Paul Soulos7b0b0ce2014-06-24 14:26:34 -0700622 }
623 }
624
Paul Soulos2a4207f2014-07-31 17:09:05 -0700625 private View createEntryView(LayoutInflater layoutInflater, final Entry entry,
626 int iconVisibility) {
627 final EntryView view = (EntryView) layoutInflater.inflate(
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700628 R.layout.expanding_entry_card_item, this, false);
629
Paul Soulos2a4207f2014-07-31 17:09:05 -0700630 view.setContextMenuInfo(entry.getEntryContextMenuInfo());
Paul Soulos23e28362014-08-29 14:57:08 -0700631 if (!TextUtils.isEmpty(entry.getPrimaryContentDescription())) {
632 view.setContentDescription(entry.getPrimaryContentDescription());
633 }
Paul Soulosea5e0b72014-07-08 18:09:44 -0700634
Paul Soulosdd7419d2014-07-15 11:22:13 -0700635 final ImageView icon = (ImageView) view.findViewById(R.id.icon);
Paul Soulos97ed5012014-07-28 16:27:12 -0700636 icon.setVisibility(iconVisibility);
Paul Souloseb64a4b2014-07-07 17:03:27 -0700637 if (entry.getIcon() != null) {
638 icon.setImageDrawable(entry.getIcon());
Paul Souloseb64a4b2014-07-07 17:03:27 -0700639 }
Paul Soulosdd7419d2014-07-15 11:22:13 -0700640 final TextView header = (TextView) view.findViewById(R.id.header);
Paul Soulos03ece862014-07-23 12:32:18 -0700641 if (!TextUtils.isEmpty(entry.getHeader())) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700642 header.setText(entry.getHeader());
643 } else {
644 header.setVisibility(View.GONE);
645 }
646
Paul Soulosdd7419d2014-07-15 11:22:13 -0700647 final TextView subHeader = (TextView) view.findViewById(R.id.sub_header);
Paul Soulos03ece862014-07-23 12:32:18 -0700648 if (!TextUtils.isEmpty(entry.getSubHeader())) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700649 subHeader.setText(entry.getSubHeader());
650 } else {
651 subHeader.setVisibility(View.GONE);
652 }
653
Paul Soulosdd7419d2014-07-15 11:22:13 -0700654 final ImageView subHeaderIcon = (ImageView) view.findViewById(R.id.icon_sub_header);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700655 if (entry.getSubHeaderIcon() != null) {
656 subHeaderIcon.setImageDrawable(entry.getSubHeaderIcon());
657 } else {
658 subHeaderIcon.setVisibility(View.GONE);
659 }
660
Paul Soulosdd7419d2014-07-15 11:22:13 -0700661 final TextView text = (TextView) view.findViewById(R.id.text);
Paul Soulos03ece862014-07-23 12:32:18 -0700662 if (!TextUtils.isEmpty(entry.getText())) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700663 text.setText(entry.getText());
664 } else {
665 text.setVisibility(View.GONE);
666 }
667
Paul Soulosdd7419d2014-07-15 11:22:13 -0700668 final ImageView textIcon = (ImageView) view.findViewById(R.id.icon_text);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700669 if (entry.getTextIcon() != null) {
670 textIcon.setImageDrawable(entry.getTextIcon());
671 } else {
672 textIcon.setVisibility(View.GONE);
673 }
674
675 if (entry.getIntent() != null) {
Paul Soulos7d22b942014-07-08 16:58:15 -0700676 view.setOnClickListener(mOnClickListener);
Paul Soulos2ed2a732014-08-12 11:58:39 -0700677 view.setTag(new EntryTag(entry.getId(), entry.getIntent()));
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700678 }
679
Paul Soulosa42ef762014-08-20 10:26:10 -0700680 if (entry.getIntent() == null && entry.getEntryContextMenuInfo() == null) {
681 // Remove the click effect
682 view.setBackground(null);
683 }
684
Paul Soulosac9b3162014-07-30 16:45:01 -0700685 // If only the header is visible, add a top margin to match icon's top margin.
686 // Also increase the space below the header for visual comfort.
Paul Soulos4b943552014-07-23 14:49:52 -0700687 if (header.getVisibility() == View.VISIBLE && subHeader.getVisibility() == View.GONE &&
688 text.getVisibility() == View.GONE) {
689 RelativeLayout.LayoutParams headerLayoutParams =
690 (RelativeLayout.LayoutParams) header.getLayoutParams();
691 headerLayoutParams.topMargin = (int) (getResources().getDimension(
Paul Soulosac9b3162014-07-30 16:45:01 -0700692 R.dimen.expanding_entry_card_item_header_only_margin_top));
693 headerLayoutParams.bottomMargin += (int) (getResources().getDimension(
694 R.dimen.expanding_entry_card_item_header_only_margin_bottom));
Paul Soulos4b943552014-07-23 14:49:52 -0700695 header.setLayoutParams(headerLayoutParams);
696 }
697
Paul Soulosedd44f02014-08-19 17:07:03 -0700698 // Adjust the top padding size for entries with an invisible icon. The padding depends on
699 // if there is a sub header or text section
700 if (iconVisibility == View.INVISIBLE &&
701 (!TextUtils.isEmpty(entry.getSubHeader()) || !TextUtils.isEmpty(entry.getText()))) {
702 view.setPaddingRelative(view.getPaddingStart(),
703 getResources().getDimensionPixelSize(
704 R.dimen.expanding_entry_card_item_no_icon_margin_top),
705 view.getPaddingEnd(),
706 view.getPaddingBottom());
707 } else if (iconVisibility == View.INVISIBLE && TextUtils.isEmpty(entry.getSubHeader())
708 && TextUtils.isEmpty(entry.getText())) {
Paul Soulosac9b3162014-07-30 16:45:01 -0700709 view.setPaddingRelative(view.getPaddingStart(), 0, view.getPaddingEnd(),
710 view.getPaddingBottom());
711 }
712
Paul Soulos48fc9122014-08-26 13:52:36 -0700713 final ImageView alternateIcon = (ImageView) view.findViewById(R.id.icon_alternate);
714 final ImageView thirdIcon = (ImageView) view.findViewById(R.id.third_icon);
715
716 if (entry.getAlternateIcon() != null && entry.getAlternateIntent() != null) {
717 alternateIcon.setImageDrawable(entry.getAlternateIcon());
718 alternateIcon.setOnClickListener(mOnClickListener);
719 alternateIcon.setTag(new EntryTag(entry.getId(), entry.getAlternateIntent()));
720 alternateIcon.setVisibility(View.VISIBLE);
721 alternateIcon.setContentDescription(entry.getAlternateContentDescription());
722 }
723
Tyler Gunn5f87e922015-08-05 14:24:52 -0700724 if (entry.getThirdIcon() != null && entry.getThirdAction() != Entry.ACTION_NONE) {
Paul Soulos48fc9122014-08-26 13:52:36 -0700725 thirdIcon.setImageDrawable(entry.getThirdIcon());
Tyler Gunn5f87e922015-08-05 14:24:52 -0700726 if (entry.getThirdAction() == Entry.ACTION_INTENT) {
727 thirdIcon.setOnClickListener(mOnClickListener);
728 thirdIcon.setTag(new EntryTag(entry.getId(), entry.getThirdIntent()));
729 } else if (entry.getThirdAction() == Entry.ACTION_CALL_WITH_SUBJECT) {
730 thirdIcon.setOnClickListener(new View.OnClickListener() {
731 @Override
732 public void onClick(View v) {
733 Object tag = v.getTag();
734 if (!(tag instanceof Bundle)) {
735 return;
736 }
737
738 Context context = getContext();
739 if (context instanceof Activity) {
740 CallSubjectDialog.start((Activity) context, entry.getThirdExtras());
741 }
742 }
743 });
744 thirdIcon.setTag(entry.getThirdExtras());
745 }
Paul Soulos48fc9122014-08-26 13:52:36 -0700746 thirdIcon.setVisibility(View.VISIBLE);
747 thirdIcon.setContentDescription(entry.getThirdContentDescription());
748 }
749
750 // Set a custom touch listener for expanding the extra icon touch areas
751 view.setOnTouchListener(new EntryTouchListener(view, alternateIcon, thirdIcon));
Paul Soulos2a4207f2014-07-31 17:09:05 -0700752 view.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
753
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700754 return view;
755 }
756
Paul Soulosc205cf12014-08-04 14:35:56 -0700757 private void updateExpandCollapseButton(CharSequence buttonText, long duration) {
758 if (mIsExpanded) {
759 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
760 "rotation", 180);
761 animator.setDuration(duration);
762 animator.start();
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700763 } else {
Paul Soulosc205cf12014-08-04 14:35:56 -0700764 final ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandCollapseArrow,
765 "rotation", 0);
766 animator.setDuration(duration);
767 animator.start();
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700768 }
769 mExpandCollapseTextView.setText(buttonText);
770 }
771
772 private void expand() {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700773 ChangeBounds boundsTransition = new ChangeBounds();
774 boundsTransition.setDuration(DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
775
776 Fade fadeIn = new Fade(Fade.IN);
777 fadeIn.setDuration(DURATION_EXPAND_ANIMATION_FADE_IN);
778 fadeIn.setStartDelay(DELAY_EXPAND_ANIMATION_FADE_IN);
779
780 TransitionSet transitionSet = new TransitionSet();
781 transitionSet.addTransition(boundsTransition);
782 transitionSet.addTransition(fadeIn);
783
Paul Soulos4cd9ed62014-09-09 16:37:07 -0700784 transitionSet.excludeTarget(R.id.text, /* exclude = */ true);
785
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700786 final ViewGroup transitionViewContainer = mAnimationViewGroup == null ?
787 this : mAnimationViewGroup;
788
789 transitionSet.addListener(new TransitionListener() {
790 @Override
791 public void onTransitionStart(Transition transition) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800792 mListener.onExpand();
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700793 }
794
795 @Override
796 public void onTransitionEnd(Transition transition) {
Brian Attwell245d3d22015-01-21 09:50:08 -0800797 mListener.onExpandDone();
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700798 }
799
800 @Override
801 public void onTransitionCancel(Transition transition) {
802 }
803
804 @Override
805 public void onTransitionPause(Transition transition) {
806 }
807
808 @Override
809 public void onTransitionResume(Transition transition) {
810 }
811 });
812
813 TransitionManager.beginDelayedTransition(transitionViewContainer, transitionSet);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700814
815 mIsExpanded = true;
Brian Attwell0d49d812014-06-06 18:01:01 -0700816 // In order to insert new entries, we may need to inflate them for the first time
Paul Soulos60e51082014-07-10 12:33:04 -0700817 inflateAllEntries(LayoutInflater.from(getContext()));
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700818 insertEntriesIntoViewGroup();
Paul Soulosc205cf12014-08-04 14:35:56 -0700819 updateExpandCollapseButton(getCollapseButtonText(),
820 DURATION_EXPAND_ANIMATION_CHANGE_BOUNDS);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700821 }
822
823 private void collapse() {
Brian Attwell245d3d22015-01-21 09:50:08 -0800824 final List<View> views = calculateEntriesToRemoveDuringCollapse();
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700825
Brian Attwell245d3d22015-01-21 09:50:08 -0800826 // This animation requires layout changes, unlike the expand() animation: the action bar
827 // might get scrolled open in order to fill empty space. As a result, we can't use
828 // ChangeBounds here. Instead manually animate view height and alpha. This isn't as
829 // efficient as the bounds and translation changes performed by ChangeBounds. Nonetheless, a
830 // reasonable frame-rate is achieved collapsing a dozen elements on a user Svelte N4. So the
831 // performance hit doesn't justify writing a less maintainable animation.
832 final AnimatorSet set = new AnimatorSet();
833 final List<Animator> animators = new ArrayList<Animator>(views.size());
834 int totalSizeChange = 0;
835 for (View viewToRemove : views) {
836 final ObjectAnimator animator = ObjectAnimator.ofObject(viewToRemove,
837 VIEW_LAYOUT_HEIGHT_PROPERTY, null, viewToRemove.getHeight(), 0);
838 totalSizeChange += viewToRemove.getHeight();
839 animator.setDuration(DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
840 animators.add(animator);
841 viewToRemove.animate().alpha(0).setDuration(DURATION_COLLAPSE_ANIMATION_FADE_OUT);
842 }
843 set.playTogether(animators);
844 set.start();
845 set.addListener(new AnimatorListener() {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700846 @Override
Brian Attwell245d3d22015-01-21 09:50:08 -0800847 public void onAnimationStart(Animator animation) {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700848 }
849
850 @Override
Brian Attwell245d3d22015-01-21 09:50:08 -0800851 public void onAnimationEnd(Animator animation) {
852 // Now that the views have been animated away, actually remove them from the view
853 // hierarchy. Reset their appearance so that they look appropriate when they
Brian Attwell05287bf2015-02-25 22:24:04 -0800854 // get added back later.
Brian Attwell245d3d22015-01-21 09:50:08 -0800855 insertEntriesIntoViewGroup();
856 for (View view : views) {
Brian Attwell05287bf2015-02-25 22:24:04 -0800857 if (view instanceof EntryView) {
858 VIEW_LAYOUT_HEIGHT_PROPERTY.set(view, LayoutParams.WRAP_CONTENT);
859 } else {
860 VIEW_LAYOUT_HEIGHT_PROPERTY.set(view, mDividerLineHeightPixels);
861 }
Brian Attwell245d3d22015-01-21 09:50:08 -0800862 view.animate().cancel();
863 view.setAlpha(1);
864 }
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700865 }
866
867 @Override
Brian Attwell245d3d22015-01-21 09:50:08 -0800868 public void onAnimationCancel(Animator animation) {
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700869 }
870
871 @Override
Brian Attwell245d3d22015-01-21 09:50:08 -0800872 public void onAnimationRepeat(Animator animation) {
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700873 }
874 });
Paul Soulos0cda9ae2014-07-23 11:27:28 -0700875
Brian Attwell245d3d22015-01-21 09:50:08 -0800876 mListener.onCollapse(totalSizeChange);
877 mIsExpanded = false;
878 updateExpandCollapseButton(getExpandButtonText(),
879 DURATION_COLLAPSE_ANIMATION_CHANGE_BOUNDS);
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700880 }
881
882 /**
883 * Returns whether the view is currently in its expanded state.
884 */
885 public boolean isExpanded() {
886 return mIsExpanded;
887 }
888
889 /**
890 * Sets the title text of this ExpandingEntryCardView.
Paul Soulos2fa69082014-07-15 12:02:30 -0700891 * @param title The title to set. A null title will result in the title being removed.
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700892 */
893 public void setTitle(String title) {
894 if (mTitleTextView == null) {
895 Log.e(TAG, "mTitleTextView is null");
896 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700897 mTitleTextView.setText(title);
Paul Soulos13217362014-08-14 12:27:08 -0700898 mTitleTextView.setVisibility(TextUtils.isEmpty(title) ? View.GONE : View.VISIBLE);
899 findViewById(R.id.title_separator).setVisibility(TextUtils.isEmpty(title) ?
900 View.GONE : View.VISIBLE);
901 // If the title is set after children have been added, reset the top entry's padding to
902 // the default. Else if the title is cleared after children have been added, set
903 // the extra top padding
904 if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
905 View firstEntry = mEntriesViewGroup.getChildAt(0);
906 firstEntry.setPadding(firstEntry.getPaddingLeft(),
907 getResources().getDimensionPixelSize(
908 R.dimen.expanding_entry_card_item_padding_top),
909 firstEntry.getPaddingRight(),
910 firstEntry.getPaddingBottom());
911 } else if (!TextUtils.isEmpty(title) && mEntriesViewGroup.getChildCount() > 0) {
912 View firstEntry = mEntriesViewGroup.getChildAt(0);
913 firstEntry.setPadding(firstEntry.getPaddingLeft(),
914 getResources().getDimensionPixelSize(
915 R.dimen.expanding_entry_card_item_padding_top) +
916 getResources().getDimensionPixelSize(
917 R.dimen.expanding_entry_card_null_title_top_extra_padding),
918 firstEntry.getPaddingRight(),
Brian Attwell88972872015-01-22 14:53:27 -0800919 firstEntry.getPaddingBottom());
Paul Soulos13217362014-08-14 12:27:08 -0700920 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -0700921 }
Paul Souloseb64a4b2014-07-07 17:03:27 -0700922
923 public boolean shouldShow() {
924 return mEntries != null && mEntries.size() > 0;
925 }
Paul Soulos2a4207f2014-07-31 17:09:05 -0700926
927 public static final class EntryView extends RelativeLayout {
928 private EntryContextMenuInfo mEntryContextMenuInfo;
929
930 public EntryView(Context context) {
931 super(context);
932 }
933
934 public EntryView(Context context, AttributeSet attrs) {
935 super(context, attrs);
936 }
937
938 public void setContextMenuInfo(EntryContextMenuInfo info) {
939 mEntryContextMenuInfo = info;
940 }
941
942 @Override
943 protected ContextMenuInfo getContextMenuInfo() {
944 return mEntryContextMenuInfo;
945 }
946 }
947
948 public static final class EntryContextMenuInfo implements ContextMenuInfo {
949 private final String mCopyText;
950 private final String mCopyLabel;
Paul Soulos97f27802014-09-08 13:55:45 -0700951 private final String mMimeType;
952 private final long mId;
953 private final boolean mIsSuperPrimary;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700954
Paul Soulos97f27802014-09-08 13:55:45 -0700955 public EntryContextMenuInfo(String copyText, String copyLabel, String mimeType, long id,
956 boolean isSuperPrimary) {
Paul Soulos2a4207f2014-07-31 17:09:05 -0700957 mCopyText = copyText;
958 mCopyLabel = copyLabel;
Paul Soulos97f27802014-09-08 13:55:45 -0700959 mMimeType = mimeType;
960 mId = id;
961 mIsSuperPrimary = isSuperPrimary;
Paul Soulos2a4207f2014-07-31 17:09:05 -0700962 }
963
964 public String getCopyText() {
965 return mCopyText;
966 }
967
968 public String getCopyLabel() {
969 return mCopyLabel;
970 }
Paul Soulos97f27802014-09-08 13:55:45 -0700971
972 public String getMimeType() {
973 return mMimeType;
974 }
975
976 public long getId() {
977 return mId;
978 }
979
980 public boolean isSuperPrimary() {
981 return mIsSuperPrimary;
982 }
Paul Soulos2a4207f2014-07-31 17:09:05 -0700983 }
Paul Soulos2ed2a732014-08-12 11:58:39 -0700984
985 static final class EntryTag {
986 private final int mId;
987 private final Intent mIntent;
988
989 public EntryTag(int id, Intent intent) {
990 mId = id;
991 mIntent = intent;
992 }
993
994 public int getId() {
995 return mId;
996 }
997
998 public Intent getIntent() {
999 return mIntent;
1000 }
1001 }
Paul Soulos48fc9122014-08-26 13:52:36 -07001002
1003 /**
1004 * This custom touch listener increases the touch area for the second and third icons, if
1005 * they are present. This is necessary to maintain other properties on an entry view, like
1006 * using a top padding on entry. Based off of {@link android.view.TouchDelegate}
1007 */
1008 private static final class EntryTouchListener implements View.OnTouchListener {
1009 private final View mEntry;
1010 private final ImageView mAlternateIcon;
1011 private final ImageView mThirdIcon;
1012 /** mTouchedView locks in a view on touch down */
1013 private View mTouchedView;
1014 /** mSlop adds some space to account for touches that are just outside the hit area */
1015 private int mSlop;
1016
1017 public EntryTouchListener(View entry, ImageView alternateIcon, ImageView thirdIcon) {
1018 mEntry = entry;
1019 mAlternateIcon = alternateIcon;
1020 mThirdIcon = thirdIcon;
1021 mSlop = ViewConfiguration.get(entry.getContext()).getScaledTouchSlop();
1022 }
1023
1024 @Override
1025 public boolean onTouch(View v, MotionEvent event) {
1026 View touchedView = mTouchedView;
1027 boolean sendToTouched = false;
1028 boolean hit = true;
1029 boolean handled = false;
1030
1031 switch (event.getAction()) {
1032 case MotionEvent.ACTION_DOWN:
1033 if (hitThirdIcon(event)) {
1034 mTouchedView = mThirdIcon;
1035 sendToTouched = true;
1036 } else if (hitAlternateIcon(event)) {
1037 mTouchedView = mAlternateIcon;
1038 sendToTouched = true;
1039 } else {
1040 mTouchedView = mEntry;
1041 sendToTouched = false;
1042 }
1043 touchedView = mTouchedView;
1044 break;
1045 case MotionEvent.ACTION_UP:
1046 case MotionEvent.ACTION_MOVE:
1047 sendToTouched = mTouchedView != null && mTouchedView != mEntry;
1048 if (sendToTouched) {
1049 final Rect slopBounds = new Rect();
1050 touchedView.getHitRect(slopBounds);
1051 slopBounds.inset(-mSlop, -mSlop);
1052 if (!slopBounds.contains((int) event.getX(), (int) event.getY())) {
1053 hit = false;
1054 }
1055 }
1056 break;
1057 case MotionEvent.ACTION_CANCEL:
1058 sendToTouched = mTouchedView != null && mTouchedView != mEntry;
1059 mTouchedView = null;
1060 break;
1061 }
1062 if (sendToTouched) {
1063 if (hit) {
1064 event.setLocation(touchedView.getWidth() / 2, touchedView.getHeight() / 2);
1065 } else {
1066 // Offset event coordinates to be outside the target view (in case it does
1067 // something like tracking pressed state)
1068 event.setLocation(-(mSlop * 2), -(mSlop * 2));
1069 }
1070 handled = touchedView.dispatchTouchEvent(event);
1071 }
1072 return handled;
1073 }
1074
1075 private boolean hitThirdIcon(MotionEvent event) {
Brian Attwellf1402272014-12-16 16:00:08 -08001076 if (mEntry.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
Paul Soulos48fc9122014-08-26 13:52:36 -07001077 return mThirdIcon.getVisibility() == View.VISIBLE &&
1078 event.getX() < mThirdIcon.getRight();
1079 } else {
1080 return mThirdIcon.getVisibility() == View.VISIBLE &&
1081 event.getX() > mThirdIcon.getLeft();
1082 }
1083 }
1084
1085 /**
1086 * Should be used after checking if third icon was hit
1087 */
1088 private boolean hitAlternateIcon(MotionEvent event) {
1089 // LayoutParams used to add the start margin to the touch area
1090 final RelativeLayout.LayoutParams alternateIconParams =
1091 (RelativeLayout.LayoutParams) mAlternateIcon.getLayoutParams();
Brian Attwellf1402272014-12-16 16:00:08 -08001092 if (mEntry.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
Paul Soulos48fc9122014-08-26 13:52:36 -07001093 return mAlternateIcon.getVisibility() == View.VISIBLE &&
1094 event.getX() < mAlternateIcon.getRight() + alternateIconParams.rightMargin;
1095 } else {
1096 return mAlternateIcon.getVisibility() == View.VISIBLE &&
1097 event.getX() > mAlternateIcon.getLeft() - alternateIconParams.leftMargin;
1098 }
1099 }
1100 }
Paul Soulos2d48b5a2014-05-29 13:56:25 -07001101}