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