blob: 345bfe5a5acdb6d826aeea560384a777117e0876 [file] [log] [blame]
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +08001/*
2 * Copyright (C) 2018 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 */
16
17package com.android.documentsui.queries;
18
19import android.animation.ObjectAnimator;
20import android.content.Context;
Austin Wangf0b6bd42019-08-28 16:05:25 +080021import android.graphics.drawable.Drawable;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080022import android.os.Bundle;
Austin Wangf0b6bd42019-08-28 16:05:25 +080023import android.provider.DocumentsContract;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080024import android.view.LayoutInflater;
25import android.view.View;
Ivan Chiang329131e2019-01-08 15:58:19 +080026import android.view.ViewGroup;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080027import android.widget.HorizontalScrollView;
28
29import androidx.annotation.NonNull;
Ivan Chiang329131e2019-01-08 15:58:19 +080030import androidx.annotation.Nullable;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080031import androidx.annotation.VisibleForTesting;
32
33import com.android.documentsui.IconUtils;
shawnline0ba46d2019-01-25 15:21:26 +080034import com.android.documentsui.MetricConsts;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080035import com.android.documentsui.R;
36import com.android.documentsui.base.MimeTypes;
37import com.android.documentsui.base.Shared;
38
39import com.google.android.material.chip.Chip;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080040import com.google.common.primitives.Ints;
41
Austin Wangf0b6bd42019-08-28 16:05:25 +080042import java.time.LocalDate;
43import java.time.ZoneId;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080044import java.util.ArrayList;
Ivan Chiang3933cc62019-02-14 13:35:07 +080045import java.util.Arrays;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080046import java.util.Collections;
47import java.util.Comparator;
48import java.util.HashMap;
49import java.util.HashSet;
50import java.util.List;
51import java.util.Map;
52import java.util.Set;
53
54/**
55 * Manages search chip behavior.
56 */
57public class SearchChipViewManager {
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080058 private static final int CHIP_MOVE_ANIMATION_DURATION = 250;
Austin Wangf0b6bd42019-08-28 16:05:25 +080059 // Defined large file as the size is larger than 10 MB.
60 private static final long LARGE_FILE_SIZE_BYTES = 10000000L;
61 // Defined a week ago as now in millis.
62 private static final long A_WEEK_AGO_MILLIS =
63 LocalDate.now().minusDays(7).atStartOfDay(ZoneId.systemDefault())
64 .toInstant()
65 .toEpochMilli();
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080066
Austin Wangf0b6bd42019-08-28 16:05:25 +080067 private static final int TYPE_IMAGES = MetricConsts.TYPE_CHIP_IMAGES;
shawnline0ba46d2019-01-25 15:21:26 +080068 private static final int TYPE_DOCUMENTS = MetricConsts.TYPE_CHIP_DOCS;
69 private static final int TYPE_AUDIO = MetricConsts.TYPE_CHIP_AUDIOS;
70 private static final int TYPE_VIDEOS = MetricConsts.TYPE_CHIP_VIDEOS;
Austin Wangf0b6bd42019-08-28 16:05:25 +080071 private static final int TYPE_LARGE_FILES = MetricConsts.TYPE_CHIP_LARGE_FILES;
72 private static final int TYPE_FROM_THIS_WEEK = MetricConsts.TYPE_CHIP_FROM_THIS_WEEK;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080073
74 private static final ChipComparator CHIP_COMPARATOR = new ChipComparator();
75
76 // we will get the icon drawable with the first mimeType
77 private static final String[] IMAGES_MIMETYPES = new String[]{"image/*"};
78 private static final String[] VIDEOS_MIMETYPES = new String[]{"video/*"};
79 private static final String[] AUDIO_MIMETYPES =
80 new String[]{"audio/*", "application/ogg", "application/x-flac"};
81 private static final String[] DOCUMENTS_MIMETYPES = new String[]{"application/*", "text/*"};
Austin Wangf0b6bd42019-08-28 16:05:25 +080082 private static final String[] EMPTY_MIMETYPES = new String[]{""};
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080083
Austin Wang986050b2019-09-17 14:54:49 +080084 private static final Map<Integer, SearchChipData> sMimeTypesChipItems = new HashMap<>();
85 private static final Map<Integer, SearchChipData> sDefaultChipItems = new HashMap<>();
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080086
Ivan Chiang329131e2019-01-08 15:58:19 +080087 private final ViewGroup mChipGroup;
Ivan Chiang78171b32019-01-18 17:01:49 +080088 private final List<Integer> mDefaultChipTypes = new ArrayList<>();
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080089 private SearchChipViewManagerListener mListener;
Ivan Chiang3933cc62019-02-14 13:35:07 +080090 private String[] mCurrentUpdateMimeTypes;
91 private boolean mIsFirstUpdateChipsReady;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080092
93 @VisibleForTesting
94 Set<SearchChipData> mCheckedChipItems = new HashSet<>();
95
96 static {
Austin Wang986050b2019-09-17 14:54:49 +080097 sMimeTypesChipItems.put(TYPE_IMAGES,
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +080098 new SearchChipData(TYPE_IMAGES, R.string.chip_title_images, IMAGES_MIMETYPES));
Austin Wang986050b2019-09-17 14:54:49 +080099 sMimeTypesChipItems.put(TYPE_DOCUMENTS,
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800100 new SearchChipData(TYPE_DOCUMENTS, R.string.chip_title_documents,
101 DOCUMENTS_MIMETYPES));
Austin Wang986050b2019-09-17 14:54:49 +0800102 sMimeTypesChipItems.put(TYPE_AUDIO,
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800103 new SearchChipData(TYPE_AUDIO, R.string.chip_title_audio, AUDIO_MIMETYPES));
Austin Wang986050b2019-09-17 14:54:49 +0800104 sMimeTypesChipItems.put(TYPE_VIDEOS,
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800105 new SearchChipData(TYPE_VIDEOS, R.string.chip_title_videos, VIDEOS_MIMETYPES));
Austin Wang986050b2019-09-17 14:54:49 +0800106 sDefaultChipItems.put(TYPE_LARGE_FILES,
Austin Wangf0b6bd42019-08-28 16:05:25 +0800107 new SearchChipData(TYPE_LARGE_FILES,
108 R.string.chip_title_large_files,
109 EMPTY_MIMETYPES));
Austin Wang986050b2019-09-17 14:54:49 +0800110 sDefaultChipItems.put(TYPE_FROM_THIS_WEEK,
Austin Wangf0b6bd42019-08-28 16:05:25 +0800111 new SearchChipData(TYPE_FROM_THIS_WEEK,
112 R.string.chip_title_from_this_week,
113 EMPTY_MIMETYPES));
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800114 }
115
Ivan Chiang329131e2019-01-08 15:58:19 +0800116 public SearchChipViewManager(@NonNull ViewGroup chipGroup) {
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800117 mChipGroup = chipGroup;
118 }
119
120 /**
121 * Restore the checked chip items by the saved state.
122 *
123 * @param savedState the saved state to restore.
124 */
125 public void restoreCheckedChipItems(Bundle savedState) {
126 final int[] chipTypes = savedState.getIntArray(Shared.EXTRA_QUERY_CHIPS);
127 if (chipTypes != null) {
128 clearCheckedChips();
129 for (int chipType : chipTypes) {
Austin Wang986050b2019-09-17 14:54:49 +0800130 SearchChipData chipData = null;
131 if (sMimeTypesChipItems.containsKey(chipType)) {
132 chipData = sMimeTypesChipItems.get(chipType);
133 } else {
134 chipData = sDefaultChipItems.get(chipType);
135 }
136
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800137 mCheckedChipItems.add(chipData);
138 setCheckedChip(chipData.getChipType());
139 }
140 }
141 }
142
143 /**
144 * Set the visibility of the chips row. If the count of chips is less than 2,
145 * we will hide the chips row.
146 *
147 * @param show the value to show/hide the chips row.
148 */
149 public void setChipsRowVisible(boolean show) {
150 // if there is only one matched chip, hide the chip group.
151 mChipGroup.setVisibility(show && mChipGroup.getChildCount() > 1 ? View.VISIBLE : View.GONE);
152 }
153
154 /**
155 * Check Whether the checked item list has contents.
156 *
157 * @return True, if the checked item list is not empty. Otherwise, return false.
158 */
159 public boolean hasCheckedItems() {
160 return !mCheckedChipItems.isEmpty();
161 }
162
163 /**
164 * Clear the checked state of Chips and the checked list.
165 */
166 public void clearCheckedChips() {
167 final int count = mChipGroup.getChildCount();
168 for (int i = 0; i < count; i++) {
169 Chip child = (Chip) mChipGroup.getChildAt(i);
170 setChipChecked(child, false /* isChecked */);
171 }
172 mCheckedChipItems.clear();
173 }
174
175 /**
Austin Wangf0b6bd42019-08-28 16:05:25 +0800176 * Get the query arguments of the checked chips.
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800177 *
Austin Wangf0b6bd42019-08-28 16:05:25 +0800178 * @return the bundle of query arguments
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800179 */
Austin Wangf0b6bd42019-08-28 16:05:25 +0800180 public Bundle getCheckedChipQueryArgs() {
181 final Bundle queryArgs = new Bundle();
182 final ArrayList<String> checkedMimeTypes = new ArrayList<>();
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800183 for (SearchChipData data : mCheckedChipItems) {
Austin Wangf0b6bd42019-08-28 16:05:25 +0800184 if (data.getChipType() == MetricConsts.TYPE_CHIP_LARGE_FILES) {
185 queryArgs.putLong(DocumentsContract.QUERY_ARG_FILE_SIZE_OVER,
186 LARGE_FILE_SIZE_BYTES);
187 } else if (data.getChipType() == MetricConsts.TYPE_CHIP_FROM_THIS_WEEK) {
188 queryArgs.putLong(DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER,
189 A_WEEK_AGO_MILLIS);
190 } else {
191 for (String mimeType : data.getMimeTypes()) {
192 checkedMimeTypes.add(mimeType);
193 }
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800194 }
195 }
Austin Wangf0b6bd42019-08-28 16:05:25 +0800196
197 if (!checkedMimeTypes.isEmpty()) {
198 queryArgs.putStringArray(DocumentsContract.QUERY_ARG_MIME_TYPES,
199 checkedMimeTypes.toArray(new String[0]));
200 }
201
202 return queryArgs;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800203 }
204
205 /**
206 * Called when owning activity is saving state to be used to restore state during creation.
207 *
208 * @param state Bundle to save state
209 */
210 public void onSaveInstanceState(Bundle state) {
211 List<Integer> checkedChipList = new ArrayList<>();
212
213 for (SearchChipData item : mCheckedChipItems) {
214 checkedChipList.add(item.getChipType());
215 }
216
217 if (checkedChipList.size() > 0) {
218 state.putIntArray(Shared.EXTRA_QUERY_CHIPS, Ints.toArray(checkedChipList));
219 }
220 }
221
222 /**
Ivan Chiang78171b32019-01-18 17:01:49 +0800223 * Initialize the search chips base on the mime types.
224 *
225 * @param acceptMimeTypes use this values to filter chips
226 */
227 public void initChipSets(String[] acceptMimeTypes) {
228 mDefaultChipTypes.clear();
Austin Wang986050b2019-09-17 14:54:49 +0800229 for (SearchChipData chipData : sMimeTypesChipItems.values()) {
Ivan Chiang78171b32019-01-18 17:01:49 +0800230 final String[] mimeTypes = chipData.getMimeTypes();
231 final boolean isMatched = MimeTypes.mimeMatches(acceptMimeTypes, mimeTypes);
232 if (isMatched) {
233 mDefaultChipTypes.add(chipData.getChipType());
234 }
235 }
236 }
237
238 /**
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800239 * Update the search chips base on the mime types.
240 *
241 * @param acceptMimeTypes use this values to filter chips
242 */
243 public void updateChips(String[] acceptMimeTypes) {
Ivan Chiang3933cc62019-02-14 13:35:07 +0800244 if (mIsFirstUpdateChipsReady && Arrays.equals(mCurrentUpdateMimeTypes, acceptMimeTypes)) {
245 return;
246 }
247
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800248 final Context context = mChipGroup.getContext();
249 mChipGroup.removeAllViews();
250
Austin Wang986050b2019-09-17 14:54:49 +0800251 final List<SearchChipData> mimeChipDataList = new ArrayList<>();
252 for (int i = 0; i < mDefaultChipTypes.size(); i++) {
253 final SearchChipData chipData = sMimeTypesChipItems.get(mDefaultChipTypes.get(i));
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800254 final String[] mimeTypes = chipData.getMimeTypes();
Austin Wang986050b2019-09-17 14:54:49 +0800255 final boolean isMatched = MimeTypes.mimeMatches(acceptMimeTypes, mimeTypes);
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800256 if (isMatched) {
Austin Wang986050b2019-09-17 14:54:49 +0800257 mimeChipDataList.add(chipData);
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800258 }
259 }
Austin Wang986050b2019-09-17 14:54:49 +0800260
261 final LayoutInflater inflater = LayoutInflater.from(context);
262 if (mimeChipDataList.size() > 1) {
263 for (int i = 0; i < mimeChipDataList.size(); i++) {
264 addChipToGroup(mChipGroup, mimeChipDataList.get(i), inflater);
265 }
266 }
267
268 for (SearchChipData chipData : sDefaultChipItems.values()) {
269 addChipToGroup(mChipGroup, chipData, inflater);
270 }
271
Ivan Chiang329131e2019-01-08 15:58:19 +0800272 reorderCheckedChips(null /* clickedChip */, false /* hasAnim */);
Ivan Chiang3933cc62019-02-14 13:35:07 +0800273 mIsFirstUpdateChipsReady = true;
274 mCurrentUpdateMimeTypes = acceptMimeTypes;
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800275 }
276
Tony Huang2a022ba2019-01-10 20:02:52 +0800277 private void addChipToGroup(ViewGroup group, SearchChipData data, LayoutInflater inflater) {
278 Chip chip = (Chip) inflater.inflate(R.layout.search_chip_item, mChipGroup, false);
279 bindChip(chip, data);
280 group.addView(chip);
281 }
282
283 /**
284 * Mirror chip group here for another chip group
285 *
286 * @param chipGroup target view group for mirror
287 */
288 public void bindMirrorGroup(ViewGroup chipGroup) {
289 final int size = mChipGroup.getChildCount();
290 if (size <= 1) {
291 chipGroup.setVisibility(View.GONE);
292 return;
293 }
294
295 chipGroup.setVisibility(View.VISIBLE);
296 chipGroup.removeAllViews();
297 final LayoutInflater inflater = LayoutInflater.from(chipGroup.getContext());
298 for (int i = 0; i < size; i++) {
299 Chip child = (Chip) mChipGroup.getChildAt(i);
300 SearchChipData item = (SearchChipData) child.getTag();
301 addChipToGroup(chipGroup, item, inflater);
302 }
303 }
304
305 /**
306 * Click behavior handle here when mirror chip clicked.
307 *
308 * @param data SearchChipData synced in mirror group
309 */
310 public void onMirrorChipClick(SearchChipData data) {
311 for (int i = 0, size = mChipGroup.getChildCount(); i < size; i++) {
312 Chip chip = (Chip) mChipGroup.getChildAt(i);
313 if (chip.getTag().equals(data)) {
314 chip.setChecked(!chip.isChecked());
315 onChipClick(chip);
316 return;
317 }
318 }
319 }
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800320
321 /**
322 * Set the listener.
323 *
324 * @param listener the listener
325 */
326 public void setSearchChipViewManagerListener(SearchChipViewManagerListener listener) {
327 mListener = listener;
328 }
329
330 private static void setChipChecked(Chip chip, boolean isChecked) {
331 chip.setChecked(isChecked);
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800332 chip.setChipIconVisible(!isChecked);
333 }
334
335 private void setCheckedChip(int chipType) {
336 final int count = mChipGroup.getChildCount();
337 for (int i = 0; i < count; i++) {
338 Chip child = (Chip) mChipGroup.getChildAt(i);
339 SearchChipData item = (SearchChipData) child.getTag();
340 if (item.getChipType() == chipType) {
341 setChipChecked(child, true /* isChecked */);
342 break;
343 }
344 }
345 }
346
347 private void onChipClick(View v) {
348 final Chip chip = (Chip) v;
Ivan Chiang329131e2019-01-08 15:58:19 +0800349
350 // We need to show/hide the chip icon in our design.
351 // When we show/hide the chip icon or do reorder animation,
352 // the ripple effect will be interrupted. So, skip ripple
353 // effect when the chip is clicked.
354 chip.getBackground().setVisible(false /* visible */, false /* restart */);
355
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800356 final SearchChipData item = (SearchChipData) chip.getTag();
357 if (chip.isChecked()) {
358 mCheckedChipItems.add(item);
359 } else {
360 mCheckedChipItems.remove(item);
361 }
362
363 setChipChecked(chip, chip.isChecked());
Ivan Chiang329131e2019-01-08 15:58:19 +0800364 reorderCheckedChips(chip, true /* hasAnim */);
365
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800366 if (mListener != null) {
shawnline0ba46d2019-01-25 15:21:26 +0800367 mListener.onChipCheckStateChanged(v);
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800368 }
369 }
370
371 private void bindChip(Chip chip, SearchChipData chipData) {
Austin Wangf0b6bd42019-08-28 16:05:25 +0800372 final Context context = mChipGroup.getContext();
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800373 chip.setTag(chipData);
Austin Wangf0b6bd42019-08-28 16:05:25 +0800374 chip.setText(context.getString(chipData.getTitleRes()));
375 Drawable chipIcon;
376 if (chipData.getChipType() == TYPE_LARGE_FILES) {
377 chipIcon = context.getDrawable(R.drawable.ic_chip_large_files);
378 } else if (chipData.getChipType() == TYPE_FROM_THIS_WEEK) {
Austin Wang8af8e962019-10-14 10:50:55 +0800379 chipIcon = context.getDrawable(R.drawable.ic_chip_from_this_week);
Austin Wangf0b6bd42019-08-28 16:05:25 +0800380 } else {
381 // get the icon drawable with the first mimeType in chipData
382 chipIcon = IconUtils.loadMimeIcon(context, chipData.getMimeTypes()[0]);
383 }
384 chip.setChipIcon(chipIcon);
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800385 chip.setOnClickListener(this::onChipClick);
386
387 if (mCheckedChipItems.contains(chipData)) {
388 setChipChecked(chip, true);
389 }
390 }
391
392 /**
393 * Reorder the chips in chip group. The checked chip has higher order.
394 *
Ivan Chiang329131e2019-01-08 15:58:19 +0800395 * @param clickedChip the clicked chip, may be null.
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800396 * @param hasAnim if true, play move animation. Otherwise, not.
397 */
Ivan Chiang329131e2019-01-08 15:58:19 +0800398 private void reorderCheckedChips(@Nullable Chip clickedChip, boolean hasAnim) {
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800399 final ArrayList<Chip> chipList = new ArrayList<>();
400 final int count = mChipGroup.getChildCount();
Ivan Chiang329131e2019-01-08 15:58:19 +0800401
402 // if the size of chips is less than 2, no need to reorder chips
403 if (count < 2) {
404 return;
405 }
406
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800407 Chip item;
Ivan Chiang329131e2019-01-08 15:58:19 +0800408 // get the default order
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800409 for (int i = 0; i < count; i++) {
410 item = (Chip) mChipGroup.getChildAt(i);
411 chipList.add(item);
Ivan Chiang329131e2019-01-08 15:58:19 +0800412 }
413
414 // sort chips
415 Collections.sort(chipList, CHIP_COMPARATOR);
416
417 if (isChipOrderMatched(mChipGroup, chipList)) {
418 // the order of chips is not changed
419 return;
420 }
421
422 final int chipSpacing = mChipGroup.getPaddingEnd();
423 final boolean isRtl = mChipGroup.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
424 float lastX = isRtl ? mChipGroup.getWidth() - chipSpacing : chipSpacing;
425
426 // remove all chips except current clicked chip to avoid losing
427 // accessibility focus.
428 for (int i = count - 1; i >= 0; i--) {
429 item = (Chip) mChipGroup.getChildAt(i);
430 if (!item.equals(clickedChip)) {
431 mChipGroup.removeView(item);
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800432 }
433 }
434
Ivan Chiang329131e2019-01-08 15:58:19 +0800435 // add sorted chips
436 for (int i = 0; i < count; i++) {
437 item = chipList.get(i);
438 if (!item.equals(clickedChip)) {
439 mChipGroup.addView(item, i);
440 }
441 }
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800442
Ivan Chiang329131e2019-01-08 15:58:19 +0800443 if (hasAnim && mChipGroup.isAttachedToWindow()) {
444 // start animation
445 for (Chip chip : chipList) {
446 if (isRtl) {
447 lastX -= chip.getMeasuredWidth();
448 }
449
450 ObjectAnimator animator = ObjectAnimator.ofFloat(chip, "x", chip.getX(), lastX);
451
452 if (isRtl) {
453 lastX -= chipSpacing;
454 } else {
455 lastX += chip.getMeasuredWidth() + chipSpacing;
456 }
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800457 animator.setDuration(CHIP_MOVE_ANIMATION_DURATION);
458 animator.start();
459 }
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800460
Ivan Chiang329131e2019-01-08 15:58:19 +0800461 // Let the first checked chip can be shown.
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800462 View parent = (View) mChipGroup.getParent();
Ivan Chiang329131e2019-01-08 15:58:19 +0800463 if (parent instanceof HorizontalScrollView) {
464 final int scrollToX = isRtl ? parent.getWidth() : 0;
465 ((HorizontalScrollView) parent).smoothScrollTo(scrollToX, 0);
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800466 }
467 }
468 }
469
Ivan Chiang329131e2019-01-08 15:58:19 +0800470 private static boolean isChipOrderMatched(ViewGroup chipGroup, ArrayList<Chip> chipList) {
471 if (chipGroup == null || chipList == null) {
472 return false;
473 }
474
475 final int chipCount = chipList.size();
476 if (chipGroup.getChildCount() != chipCount) {
477 return false;
478 }
479 for (int i = 0; i < chipCount; i++) {
480 if (!chipList.get(i).equals(chipGroup.getChildAt(i))) {
481 return false;
482 }
483 }
484 return true;
485 }
486
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800487 /**
488 * The listener of SearchChipViewManager.
489 */
490 public interface SearchChipViewManagerListener {
491 /**
492 * It will be triggered when the checked state of chips changes.
493 */
shawnline0ba46d2019-01-25 15:21:26 +0800494 void onChipCheckStateChanged(View v);
Ivan Chiangf0ea0ed2018-12-12 11:10:01 +0800495 }
496
497 private static class ChipComparator implements Comparator<Chip> {
498
499 @Override
500 public int compare(Chip lhs, Chip rhs) {
501 return (lhs.isChecked() == rhs.isChecked()) ? 0 : (lhs.isChecked() ? -1 : 1);
502 }
503 }
504}