1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs.customize; 16 17 import android.content.ComponentName; 18 import android.content.Context; 19 import android.content.res.Resources; 20 import android.graphics.Canvas; 21 import android.graphics.Rect; 22 import android.graphics.drawable.Drawable; 23 import android.os.Handler; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.View.OnClickListener; 27 import android.view.View.OnLayoutChangeListener; 28 import android.view.ViewGroup; 29 import android.widget.FrameLayout; 30 import android.widget.TextView; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.core.view.AccessibilityDelegateCompat; 35 import androidx.core.view.ViewCompat; 36 import androidx.recyclerview.widget.GridLayoutManager; 37 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup; 38 import androidx.recyclerview.widget.ItemTouchHelper; 39 import androidx.recyclerview.widget.RecyclerView; 40 import androidx.recyclerview.widget.RecyclerView.ItemDecoration; 41 import androidx.recyclerview.widget.RecyclerView.State; 42 import androidx.recyclerview.widget.RecyclerView.ViewHolder; 43 44 import com.android.internal.logging.UiEventLogger; 45 import com.android.systemui.R; 46 import com.android.systemui.qs.QSEditEvent; 47 import com.android.systemui.qs.QSTileHost; 48 import com.android.systemui.qs.customize.TileAdapter.Holder; 49 import com.android.systemui.qs.customize.TileQueryHelper.TileInfo; 50 import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener; 51 import com.android.systemui.qs.dagger.QSScope; 52 import com.android.systemui.qs.dagger.QSThemedContext; 53 import com.android.systemui.qs.external.CustomTile; 54 import com.android.systemui.qs.tileimpl.QSIconViewImpl; 55 import com.android.systemui.qs.tileimpl.QSTileViewImpl; 56 57 import java.util.ArrayList; 58 import java.util.List; 59 60 import javax.inject.Inject; 61 62 /** */ 63 @QSScope 64 public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener { 65 private static final long DRAG_LENGTH = 100; 66 private static final float DRAG_SCALE = 1.2f; 67 public static final long MOVE_DURATION = 150; 68 69 private static final int TYPE_TILE = 0; 70 private static final int TYPE_EDIT = 1; 71 private static final int TYPE_ACCESSIBLE_DROP = 2; 72 private static final int TYPE_HEADER = 3; 73 private static final int TYPE_DIVIDER = 4; 74 75 private static final long EDIT_ID = 10000; 76 private static final long DIVIDER_ID = 20000; 77 78 private static final int ACTION_NONE = 0; 79 private static final int ACTION_ADD = 1; 80 private static final int ACTION_MOVE = 2; 81 82 private static final int NUM_COLUMNS_ID = R.integer.quick_settings_num_columns; 83 84 private final Context mContext; 85 86 private final Handler mHandler = new Handler(); 87 private final List<TileInfo> mTiles = new ArrayList<>(); 88 private final ItemTouchHelper mItemTouchHelper; 89 private ItemDecoration mDecoration; 90 private final MarginTileDecoration mMarginDecoration; 91 private final int mMinNumTiles; 92 private final QSTileHost mHost; 93 private int mEditIndex; 94 private int mTileDividerIndex; 95 private int mFocusIndex; 96 97 private boolean mNeedsFocus; 98 private List<String> mCurrentSpecs; 99 private List<TileInfo> mOtherTiles; 100 private List<TileInfo> mAllTiles; 101 102 private Holder mCurrentDrag; 103 private int mAccessibilityAction = ACTION_NONE; 104 private int mAccessibilityFromIndex; 105 private final UiEventLogger mUiEventLogger; 106 private final AccessibilityDelegateCompat mAccessibilityDelegate; 107 private RecyclerView mRecyclerView; 108 private int mNumColumns; 109 110 @Inject TileAdapter( @SThemedContext Context context, QSTileHost qsHost, UiEventLogger uiEventLogger)111 public TileAdapter( 112 @QSThemedContext Context context, 113 QSTileHost qsHost, 114 UiEventLogger uiEventLogger) { 115 mContext = context; 116 mHost = qsHost; 117 mUiEventLogger = uiEventLogger; 118 mItemTouchHelper = new ItemTouchHelper(mCallbacks); 119 mDecoration = new TileItemDecoration(context); 120 mMarginDecoration = new MarginTileDecoration(); 121 mMinNumTiles = context.getResources().getInteger(R.integer.quick_settings_min_num_tiles); 122 mNumColumns = context.getResources().getInteger(NUM_COLUMNS_ID); 123 mAccessibilityDelegate = new TileAdapterDelegate(); 124 mSizeLookup.setSpanIndexCacheEnabled(true); 125 } 126 127 @Override onAttachedToRecyclerView(@onNull RecyclerView recyclerView)128 public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { 129 mRecyclerView = recyclerView; 130 } 131 132 @Override onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)133 public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { 134 mRecyclerView = null; 135 } 136 137 /** 138 * Update the number of columns to show, from resources. 139 * 140 * @return {@code true} if the number of columns changed, {@code false} otherwise 141 */ updateNumColumns()142 public boolean updateNumColumns() { 143 int numColumns = mContext.getResources().getInteger(NUM_COLUMNS_ID); 144 if (numColumns != mNumColumns) { 145 mNumColumns = numColumns; 146 return true; 147 } else { 148 return false; 149 } 150 } 151 getNumColumns()152 public int getNumColumns() { 153 return mNumColumns; 154 } 155 getItemTouchHelper()156 public ItemTouchHelper getItemTouchHelper() { 157 return mItemTouchHelper; 158 } 159 getItemDecoration()160 public ItemDecoration getItemDecoration() { 161 return mDecoration; 162 } 163 getMarginItemDecoration()164 public ItemDecoration getMarginItemDecoration() { 165 return mMarginDecoration; 166 } 167 changeHalfMargin(int halfMargin)168 public void changeHalfMargin(int halfMargin) { 169 mMarginDecoration.setHalfMargin(halfMargin); 170 } 171 saveSpecs(QSTileHost host)172 public void saveSpecs(QSTileHost host) { 173 List<String> newSpecs = new ArrayList<>(); 174 clearAccessibilityState(); 175 for (int i = 1; i < mTiles.size() && mTiles.get(i) != null; i++) { 176 newSpecs.add(mTiles.get(i).spec); 177 } 178 host.changeTiles(mCurrentSpecs, newSpecs); 179 mCurrentSpecs = newSpecs; 180 } 181 clearAccessibilityState()182 private void clearAccessibilityState() { 183 if (mAccessibilityAction == ACTION_ADD) { 184 // Remove blank tile from last spot 185 mTiles.remove(--mEditIndex); 186 // Update the tile divider position 187 notifyDataSetChanged(); 188 } 189 mAccessibilityAction = ACTION_NONE; 190 } 191 192 /** */ resetTileSpecs(List<String> specs)193 public void resetTileSpecs(List<String> specs) { 194 // Notify the host so the tiles get removed callbacks. 195 mHost.changeTiles(mCurrentSpecs, specs); 196 setTileSpecs(specs); 197 } 198 setTileSpecs(List<String> currentSpecs)199 public void setTileSpecs(List<String> currentSpecs) { 200 if (currentSpecs.equals(mCurrentSpecs)) { 201 return; 202 } 203 mCurrentSpecs = currentSpecs; 204 recalcSpecs(); 205 } 206 207 @Override onTilesChanged(List<TileInfo> tiles)208 public void onTilesChanged(List<TileInfo> tiles) { 209 mAllTiles = tiles; 210 recalcSpecs(); 211 } 212 recalcSpecs()213 private void recalcSpecs() { 214 if (mCurrentSpecs == null || mAllTiles == null) { 215 return; 216 } 217 mOtherTiles = new ArrayList<TileInfo>(mAllTiles); 218 mTiles.clear(); 219 mTiles.add(null); 220 for (int i = 0; i < mCurrentSpecs.size(); i++) { 221 final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i)); 222 if (tile != null) { 223 mTiles.add(tile); 224 } 225 } 226 mTiles.add(null); 227 for (int i = 0; i < mOtherTiles.size(); i++) { 228 final TileInfo tile = mOtherTiles.get(i); 229 if (tile.isSystem) { 230 mOtherTiles.remove(i--); 231 mTiles.add(tile); 232 } 233 } 234 mTileDividerIndex = mTiles.size(); 235 mTiles.add(null); 236 mTiles.addAll(mOtherTiles); 237 updateDividerLocations(); 238 notifyDataSetChanged(); 239 } 240 getAndRemoveOther(String s)241 private TileInfo getAndRemoveOther(String s) { 242 for (int i = 0; i < mOtherTiles.size(); i++) { 243 if (mOtherTiles.get(i).spec.equals(s)) { 244 return mOtherTiles.remove(i); 245 } 246 } 247 return null; 248 } 249 250 @Override getItemViewType(int position)251 public int getItemViewType(int position) { 252 if (position == 0) { 253 return TYPE_HEADER; 254 } 255 if (mAccessibilityAction == ACTION_ADD && position == mEditIndex - 1) { 256 return TYPE_ACCESSIBLE_DROP; 257 } 258 if (position == mTileDividerIndex) { 259 return TYPE_DIVIDER; 260 } 261 if (mTiles.get(position) == null) { 262 return TYPE_EDIT; 263 } 264 return TYPE_TILE; 265 } 266 267 @Override onCreateViewHolder(ViewGroup parent, int viewType)268 public Holder onCreateViewHolder(ViewGroup parent, int viewType) { 269 final Context context = parent.getContext(); 270 LayoutInflater inflater = LayoutInflater.from(context); 271 if (viewType == TYPE_HEADER) { 272 return new Holder(inflater.inflate(R.layout.qs_customize_header, parent, false)); 273 } 274 if (viewType == TYPE_DIVIDER) { 275 return new Holder(inflater.inflate(R.layout.qs_customize_tile_divider, parent, false)); 276 } 277 if (viewType == TYPE_EDIT) { 278 return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false)); 279 } 280 FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent, 281 false); 282 View view = new CustomizeTileView(context, new QSIconViewImpl(context)); 283 frame.addView(view); 284 return new Holder(frame); 285 } 286 287 @Override getItemCount()288 public int getItemCount() { 289 return mTiles.size(); 290 } 291 292 @Override onFailedToRecycleView(Holder holder)293 public boolean onFailedToRecycleView(Holder holder) { 294 holder.stopDrag(); 295 holder.clearDrag(); 296 return true; 297 } 298 setSelectableForHeaders(View view)299 private void setSelectableForHeaders(View view) { 300 final boolean selectable = mAccessibilityAction == ACTION_NONE; 301 view.setFocusable(selectable); 302 view.setImportantForAccessibility(selectable 303 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 304 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 305 view.setFocusableInTouchMode(selectable); 306 } 307 308 @Override onBindViewHolder(final Holder holder, int position)309 public void onBindViewHolder(final Holder holder, int position) { 310 if (holder.getItemViewType() == TYPE_HEADER) { 311 setSelectableForHeaders(holder.itemView); 312 return; 313 } 314 if (holder.getItemViewType() == TYPE_DIVIDER) { 315 holder.itemView.setVisibility(mTileDividerIndex < mTiles.size() - 1 ? View.VISIBLE 316 : View.INVISIBLE); 317 return; 318 } 319 if (holder.getItemViewType() == TYPE_EDIT) { 320 final String titleText; 321 Resources res = mContext.getResources(); 322 if (mCurrentDrag == null) { 323 titleText = res.getString(R.string.drag_to_add_tiles); 324 } else if (!canRemoveTiles() && mCurrentDrag.getAdapterPosition() < mEditIndex) { 325 titleText = res.getString(R.string.drag_to_remove_disabled, mMinNumTiles); 326 } else { 327 titleText = res.getString(R.string.drag_to_remove_tiles); 328 } 329 330 ((TextView) holder.itemView.findViewById(android.R.id.title)).setText(titleText); 331 setSelectableForHeaders(holder.itemView); 332 333 return; 334 } 335 if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) { 336 holder.mTileView.setClickable(true); 337 holder.mTileView.setFocusable(true); 338 holder.mTileView.setFocusableInTouchMode(true); 339 holder.mTileView.setVisibility(View.VISIBLE); 340 holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 341 holder.mTileView.setContentDescription(mContext.getString( 342 R.string.accessibility_qs_edit_tile_add_to_position, position)); 343 holder.mTileView.setOnClickListener(new OnClickListener() { 344 @Override 345 public void onClick(View v) { 346 selectPosition(holder.getLayoutPosition()); 347 } 348 }); 349 focusOnHolder(holder); 350 return; 351 } 352 353 TileInfo info = mTiles.get(position); 354 355 final boolean selectable = 0 < position && position < mEditIndex; 356 if (selectable && mAccessibilityAction == ACTION_ADD) { 357 info.state.contentDescription = mContext.getString( 358 R.string.accessibility_qs_edit_tile_add_to_position, position); 359 } else if (selectable && mAccessibilityAction == ACTION_MOVE) { 360 info.state.contentDescription = mContext.getString( 361 R.string.accessibility_qs_edit_tile_move_to_position, position); 362 } else { 363 info.state.contentDescription = info.state.label; 364 } 365 info.state.expandedAccessibilityClassName = ""; 366 367 // The holder has a tileView, therefore this call is not null 368 holder.getTileAsCustomizeView().changeState(info.state); 369 holder.getTileAsCustomizeView().setShowAppLabel(position > mEditIndex && !info.isSystem); 370 // Don't show the side view for third party tiles, as we don't have the actual state. 371 holder.getTileAsCustomizeView().setShowSideView(position < mEditIndex || info.isSystem); 372 holder.mTileView.setSelected(true); 373 holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 374 holder.mTileView.setClickable(true); 375 holder.mTileView.setOnClickListener(null); 376 holder.mTileView.setFocusable(true); 377 holder.mTileView.setFocusableInTouchMode(true); 378 379 if (mAccessibilityAction != ACTION_NONE) { 380 holder.mTileView.setClickable(selectable); 381 holder.mTileView.setFocusable(selectable); 382 holder.mTileView.setFocusableInTouchMode(selectable); 383 holder.mTileView.setImportantForAccessibility(selectable 384 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 385 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 386 if (selectable) { 387 holder.mTileView.setOnClickListener(new OnClickListener() { 388 @Override 389 public void onClick(View v) { 390 int position = holder.getLayoutPosition(); 391 if (position == RecyclerView.NO_POSITION) return; 392 if (mAccessibilityAction != ACTION_NONE) { 393 selectPosition(position); 394 } 395 } 396 }); 397 } 398 } 399 if (position == mFocusIndex) { 400 focusOnHolder(holder); 401 } 402 } 403 404 private void focusOnHolder(Holder holder) { 405 if (mNeedsFocus) { 406 // Wait for this to get laid out then set its focus. 407 // Ensure that tile gets laid out so we get the callback. 408 holder.mTileView.requestLayout(); 409 holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() { 410 @Override 411 public void onLayoutChange(View v, int left, int top, int right, int bottom, 412 int oldLeft, int oldTop, int oldRight, int oldBottom) { 413 holder.mTileView.removeOnLayoutChangeListener(this); 414 holder.mTileView.requestFocus(); 415 if (mAccessibilityAction == ACTION_NONE) { 416 holder.mTileView.clearFocus(); 417 } 418 } 419 }); 420 mNeedsFocus = false; 421 mFocusIndex = RecyclerView.NO_POSITION; 422 } 423 } 424 425 private boolean canRemoveTiles() { 426 return mCurrentSpecs.size() > mMinNumTiles; 427 } 428 429 private void selectPosition(int position) { 430 if (mAccessibilityAction == ACTION_ADD) { 431 // Remove the placeholder. 432 mTiles.remove(mEditIndex--); 433 } 434 mAccessibilityAction = ACTION_NONE; 435 move(mAccessibilityFromIndex, position, false); 436 mFocusIndex = position; 437 mNeedsFocus = true; 438 notifyDataSetChanged(); 439 } 440 441 private void startAccessibleAdd(int position) { 442 mAccessibilityFromIndex = position; 443 mAccessibilityAction = ACTION_ADD; 444 // Add placeholder for last slot. 445 mTiles.add(mEditIndex++, null); 446 // Update the tile divider position 447 mTileDividerIndex++; 448 mFocusIndex = mEditIndex - 1; 449 mNeedsFocus = true; 450 if (mRecyclerView != null) { 451 mRecyclerView.post(() -> mRecyclerView.smoothScrollToPosition(mFocusIndex)); 452 } 453 notifyDataSetChanged(); 454 } 455 456 private void startAccessibleMove(int position) { 457 mAccessibilityFromIndex = position; 458 mAccessibilityAction = ACTION_MOVE; 459 mFocusIndex = position; 460 mNeedsFocus = true; 461 notifyDataSetChanged(); 462 } 463 464 private boolean canRemoveFromPosition(int position) { 465 return canRemoveTiles() && isCurrentTile(position); 466 } 467 468 private boolean isCurrentTile(int position) { 469 return position < mEditIndex; 470 } 471 472 private boolean canAddFromPosition(int position) { 473 return position > mEditIndex; 474 } 475 476 private boolean addFromPosition(int position) { 477 if (!canAddFromPosition(position)) return false; 478 move(position, mEditIndex); 479 return true; 480 } 481 482 private boolean removeFromPosition(int position) { 483 if (!canRemoveFromPosition(position)) return false; 484 TileInfo info = mTiles.get(position); 485 move(position, info.isSystem ? mEditIndex : mTileDividerIndex); 486 return true; 487 } 488 489 public SpanSizeLookup getSizeLookup() { 490 return mSizeLookup; 491 } 492 493 private boolean move(int from, int to) { 494 return move(from, to, true); 495 } 496 497 private boolean move(int from, int to, boolean notify) { 498 if (to == from) { 499 return true; 500 } 501 move(from, to, mTiles, notify); 502 updateDividerLocations(); 503 if (to >= mEditIndex) { 504 mUiEventLogger.log(QSEditEvent.QS_EDIT_REMOVE, 0, strip(mTiles.get(to))); 505 } else if (from >= mEditIndex) { 506 mUiEventLogger.log(QSEditEvent.QS_EDIT_ADD, 0, strip(mTiles.get(to))); 507 } else { 508 mUiEventLogger.log(QSEditEvent.QS_EDIT_MOVE, 0, strip(mTiles.get(to))); 509 } 510 saveSpecs(mHost); 511 return true; 512 } 513 updateDividerLocations()514 private void updateDividerLocations() { 515 // The first null is the header label (index 0) so we can skip it, 516 // the second null is the edit tiles label, the third null is the tile divider. 517 // If there is no third null, then there are no non-system tiles. 518 mEditIndex = -1; 519 mTileDividerIndex = mTiles.size(); 520 for (int i = 1; i < mTiles.size(); i++) { 521 if (mTiles.get(i) == null) { 522 if (mEditIndex == -1) { 523 mEditIndex = i; 524 } else { 525 mTileDividerIndex = i; 526 } 527 } 528 } 529 if (mTiles.size() - 1 == mTileDividerIndex) { 530 notifyItemChanged(mTileDividerIndex); 531 } 532 } 533 strip(TileInfo tileInfo)534 private static String strip(TileInfo tileInfo) { 535 String spec = tileInfo.spec; 536 if (spec.startsWith(CustomTile.PREFIX)) { 537 ComponentName component = CustomTile.getComponentFromSpec(spec); 538 return component.getPackageName(); 539 } 540 return spec; 541 } 542 move(int from, int to, List<T> list, boolean notify)543 private <T> void move(int from, int to, List<T> list, boolean notify) { 544 list.add(to, list.remove(from)); 545 if (notify) { 546 notifyItemMoved(from, to); 547 } 548 } 549 550 public class Holder extends ViewHolder { 551 private QSTileViewImpl mTileView; 552 Holder(View itemView)553 public Holder(View itemView) { 554 super(itemView); 555 if (itemView instanceof FrameLayout) { 556 mTileView = (QSTileViewImpl) ((FrameLayout) itemView).getChildAt(0); 557 mTileView.getIcon().disableAnimation(); 558 mTileView.setTag(this); 559 ViewCompat.setAccessibilityDelegate(mTileView, mAccessibilityDelegate); 560 } 561 } 562 563 @Nullable getTileAsCustomizeView()564 public CustomizeTileView getTileAsCustomizeView() { 565 return (CustomizeTileView) mTileView; 566 } 567 clearDrag()568 public void clearDrag() { 569 itemView.clearAnimation(); 570 itemView.setScaleX(1); 571 itemView.setScaleY(1); 572 } 573 startDrag()574 public void startDrag() { 575 itemView.animate() 576 .setDuration(DRAG_LENGTH) 577 .scaleX(DRAG_SCALE) 578 .scaleY(DRAG_SCALE); 579 } 580 stopDrag()581 public void stopDrag() { 582 itemView.animate() 583 .setDuration(DRAG_LENGTH) 584 .scaleX(1) 585 .scaleY(1); 586 } 587 canRemove()588 boolean canRemove() { 589 return canRemoveFromPosition(getLayoutPosition()); 590 } 591 canAdd()592 boolean canAdd() { 593 return canAddFromPosition(getLayoutPosition()); 594 } 595 toggleState()596 void toggleState() { 597 if (canAdd()) { 598 add(); 599 } else { 600 remove(); 601 } 602 } 603 add()604 private void add() { 605 if (addFromPosition(getLayoutPosition())) { 606 itemView.announceForAccessibility( 607 itemView.getContext().getText(R.string.accessibility_qs_edit_tile_added)); 608 } 609 } 610 remove()611 private void remove() { 612 if (removeFromPosition(getLayoutPosition())) { 613 itemView.announceForAccessibility( 614 itemView.getContext().getText(R.string.accessibility_qs_edit_tile_removed)); 615 } 616 } 617 isCurrentTile()618 boolean isCurrentTile() { 619 return TileAdapter.this.isCurrentTile(getLayoutPosition()); 620 } 621 startAccessibleAdd()622 void startAccessibleAdd() { 623 TileAdapter.this.startAccessibleAdd(getLayoutPosition()); 624 } 625 startAccessibleMove()626 void startAccessibleMove() { 627 TileAdapter.this.startAccessibleMove(getLayoutPosition()); 628 } 629 canTakeAccessibleAction()630 boolean canTakeAccessibleAction() { 631 return mAccessibilityAction == ACTION_NONE; 632 } 633 } 634 635 private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() { 636 @Override 637 public int getSpanSize(int position) { 638 final int type = getItemViewType(position); 639 if (type == TYPE_EDIT || type == TYPE_DIVIDER || type == TYPE_HEADER) { 640 return mNumColumns; 641 } else { 642 return 1; 643 } 644 } 645 }; 646 647 private class TileItemDecoration extends ItemDecoration { 648 private final Drawable mDrawable; 649 TileItemDecoration(Context context)650 private TileItemDecoration(Context context) { 651 mDrawable = context.getDrawable(R.drawable.qs_customize_tile_decoration); 652 } 653 654 @Override onDraw(Canvas c, RecyclerView parent, State state)655 public void onDraw(Canvas c, RecyclerView parent, State state) { 656 super.onDraw(c, parent, state); 657 658 final int childCount = parent.getChildCount(); 659 final int width = parent.getWidth(); 660 final int bottom = parent.getBottom(); 661 for (int i = 0; i < childCount; i++) { 662 final View child = parent.getChildAt(i); 663 final ViewHolder holder = parent.getChildViewHolder(child); 664 // Do not draw background for the holder that's currently being dragged 665 if (holder == mCurrentDrag) { 666 continue; 667 } 668 // Do not draw background for holders before the edit index (header and current 669 // tiles) 670 if (holder.getAdapterPosition() == 0 || 671 holder.getAdapterPosition() < mEditIndex && !(child instanceof TextView)) { 672 continue; 673 } 674 675 final int top = child.getTop() + Math.round(ViewCompat.getTranslationY(child)); 676 mDrawable.setBounds(0, top, width, bottom); 677 mDrawable.draw(c); 678 break; 679 } 680 } 681 } 682 683 private static class MarginTileDecoration extends ItemDecoration { 684 private int mHalfMargin; 685 setHalfMargin(int halfMargin)686 public void setHalfMargin(int halfMargin) { 687 mHalfMargin = halfMargin; 688 } 689 690 @Override getItemOffsets(@onNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull State state)691 public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, 692 @NonNull RecyclerView parent, @NonNull State state) { 693 if (parent.getLayoutManager() == null) return; 694 695 GridLayoutManager lm = ((GridLayoutManager) parent.getLayoutManager()); 696 int column = ((GridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex(); 697 698 if (view instanceof TextView) { 699 super.getItemOffsets(outRect, view, parent, state); 700 } else { 701 if (column != 0 && column != lm.getSpanCount() - 1) { 702 // In a column that's not leftmost or rightmost (half of the margin between 703 // columns). 704 outRect.left = mHalfMargin; 705 outRect.right = mHalfMargin; 706 } else { 707 // Leftmost or rightmost column 708 if (parent.isLayoutRtl()) { 709 if (column == 0) { 710 // Rightmost column 711 outRect.left = mHalfMargin; 712 outRect.right = 0; 713 } else { 714 // Leftmost column 715 outRect.left = 0; 716 outRect.right = mHalfMargin; 717 } 718 } else { 719 // Non RTL 720 if (column == 0) { 721 // Leftmost column 722 outRect.left = 0; 723 outRect.right = mHalfMargin; 724 } else { 725 // Rightmost column 726 outRect.left = mHalfMargin; 727 outRect.right = 0; 728 } 729 } 730 } 731 } 732 } 733 } 734 735 private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() { 736 737 @Override 738 public boolean isLongPressDragEnabled() { 739 return true; 740 } 741 742 @Override 743 public boolean isItemViewSwipeEnabled() { 744 return false; 745 } 746 747 @Override 748 public void onSelectedChanged(ViewHolder viewHolder, int actionState) { 749 super.onSelectedChanged(viewHolder, actionState); 750 if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) { 751 viewHolder = null; 752 } 753 if (viewHolder == mCurrentDrag) return; 754 if (mCurrentDrag != null) { 755 int position = mCurrentDrag.getAdapterPosition(); 756 if (position == RecyclerView.NO_POSITION) return; 757 TileInfo info = mTiles.get(position); 758 ((CustomizeTileView) mCurrentDrag.mTileView).setShowAppLabel( 759 position > mEditIndex && !info.isSystem); 760 mCurrentDrag.stopDrag(); 761 mCurrentDrag = null; 762 } 763 if (viewHolder != null) { 764 mCurrentDrag = (Holder) viewHolder; 765 mCurrentDrag.startDrag(); 766 } 767 mHandler.post(new Runnable() { 768 @Override 769 public void run() { 770 notifyItemChanged(mEditIndex); 771 } 772 }); 773 } 774 775 @Override 776 public boolean canDropOver(RecyclerView recyclerView, ViewHolder current, 777 ViewHolder target) { 778 final int position = target.getAdapterPosition(); 779 if (position == 0 || position == RecyclerView.NO_POSITION){ 780 return false; 781 } 782 if (!canRemoveTiles() && current.getAdapterPosition() < mEditIndex) { 783 return position < mEditIndex; 784 } 785 return position <= mEditIndex + 1; 786 } 787 788 @Override 789 public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) { 790 switch (viewHolder.getItemViewType()) { 791 case TYPE_EDIT: 792 case TYPE_DIVIDER: 793 case TYPE_HEADER: 794 // Fall through 795 return makeMovementFlags(0, 0); 796 default: 797 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN 798 | ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT; 799 return makeMovementFlags(dragFlags, 0); 800 } 801 } 802 803 @Override 804 public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) { 805 int from = viewHolder.getAdapterPosition(); 806 int to = target.getAdapterPosition(); 807 if (from == 0 || from == RecyclerView.NO_POSITION || 808 to == 0 || to == RecyclerView.NO_POSITION) { 809 return false; 810 } 811 return move(from, to); 812 } 813 814 @Override 815 public void onSwiped(ViewHolder viewHolder, int direction) { 816 } 817 818 // Just in case, make sure to animate to base state. 819 @Override 820 public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { 821 ((Holder) viewHolder).stopDrag(); 822 super.clearView(recyclerView, viewHolder); 823 } 824 }; 825 } 826