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