TileAdapter.java revision 96defbe2b123b316f5aecc0e9a3504b26f2bc54c
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 15package com.android.systemui.qs.customize; 16 17import android.app.AlertDialog; 18import android.app.AlertDialog.Builder; 19import android.content.ComponentName; 20import android.content.Context; 21import android.content.DialogInterface; 22import android.graphics.Canvas; 23import android.graphics.drawable.ColorDrawable; 24import android.os.Handler; 25import android.support.v4.view.ViewCompat; 26import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; 27import android.support.v7.widget.RecyclerView; 28import android.support.v7.widget.RecyclerView.ItemDecoration; 29import android.support.v7.widget.RecyclerView.State; 30import android.support.v7.widget.RecyclerView.ViewHolder; 31import android.support.v7.widget.helper.ItemTouchHelper; 32import android.view.LayoutInflater; 33import android.view.View; 34import android.view.View.OnClickListener; 35import android.view.View.OnLayoutChangeListener; 36import android.view.ViewGroup; 37import android.view.accessibility.AccessibilityManager; 38import android.widget.FrameLayout; 39import android.widget.TextView; 40import com.android.internal.logging.MetricsLogger; 41import com.android.internal.logging.MetricsProto; 42import com.android.systemui.R; 43import com.android.systemui.qs.QSIconView; 44import com.android.systemui.qs.QSTileView; 45import com.android.systemui.qs.customize.TileAdapter.Holder; 46import com.android.systemui.qs.customize.TileQueryHelper.TileInfo; 47import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener; 48import com.android.systemui.qs.external.CustomTile; 49import com.android.systemui.statusbar.phone.QSTileHost; 50import com.android.systemui.statusbar.phone.SystemUIDialog; 51 52import java.util.ArrayList; 53import java.util.List; 54 55public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener { 56 57 private static final long DRAG_LENGTH = 100; 58 private static final float DRAG_SCALE = 1.2f; 59 public static final long MOVE_DURATION = 150; 60 61 private static final int TYPE_TILE = 0; 62 private static final int TYPE_EDIT = 1; 63 private static final int TYPE_ACCESSIBLE_DROP = 2; 64 65 private final Context mContext; 66 67 private final Handler mHandler = new Handler(); 68 private final List<TileInfo> mTiles = new ArrayList<>(); 69 private final ItemTouchHelper mItemTouchHelper; 70 private final AccessibilityManager mAccessibilityManager; 71 private int mDividerIndex; 72 private boolean mNeedsFocus; 73 private List<String> mCurrentSpecs; 74 private List<TileInfo> mOtherTiles; 75 private List<TileInfo> mAllTiles; 76 77 private Holder mCurrentDrag; 78 private boolean mAccessibilityMoving; 79 private int mAccessibilityFromIndex; 80 81 public TileAdapter(Context context) { 82 mContext = context; 83 mAccessibilityManager = context.getSystemService(AccessibilityManager.class); 84 mItemTouchHelper = new ItemTouchHelper(mCallbacks); 85 setHasStableIds(true); 86 } 87 88 @Override 89 public long getItemId(int position) { 90 return mTiles.get(position) != null ? mAllTiles.indexOf(mTiles.get(position)) : -1; 91 } 92 93 public ItemTouchHelper getItemTouchHelper() { 94 return mItemTouchHelper; 95 } 96 97 public ItemDecoration getItemDecoration() { 98 return mDecoration; 99 } 100 101 public void saveSpecs(QSTileHost host) { 102 List<String> newSpecs = new ArrayList<>(); 103 for (int i = 0; mTiles.get(i) != null; i++) { 104 newSpecs.add(mTiles.get(i).spec); 105 } 106 host.changeTiles(mCurrentSpecs, newSpecs); 107 setTileSpecs(newSpecs); 108 } 109 110 public void setTileSpecs(List<String> currentSpecs) { 111 mCurrentSpecs = currentSpecs; 112 recalcSpecs(); 113 } 114 115 @Override 116 public void onTilesChanged(List<TileInfo> tiles) { 117 mAllTiles = tiles; 118 recalcSpecs(); 119 } 120 121 private void recalcSpecs() { 122 if (mCurrentSpecs == null || mAllTiles == null) { 123 return; 124 } 125 mOtherTiles = new ArrayList<TileInfo>(mAllTiles); 126 mTiles.clear(); 127 for (int i = 0; i < mCurrentSpecs.size(); i++) { 128 final TileInfo tile = getAndRemoveOther(mCurrentSpecs.get(i)); 129 if (tile != null) { 130 mTiles.add(tile); 131 } 132 } 133 mTiles.add(null); 134 mTiles.addAll(mOtherTiles); 135 mDividerIndex = mTiles.indexOf(null); 136 notifyDataSetChanged(); 137 } 138 139 private TileInfo getAndRemoveOther(String s) { 140 for (int i = 0; i < mOtherTiles.size(); i++) { 141 if (mOtherTiles.get(i).spec.equals(s)) { 142 return mOtherTiles.remove(i); 143 } 144 } 145 return null; 146 } 147 148 @Override 149 public int getItemViewType(int position) { 150 if (mAccessibilityMoving && position == mDividerIndex - 1) { 151 return TYPE_ACCESSIBLE_DROP; 152 } 153 if (mTiles.get(position) == null) { 154 return TYPE_EDIT; 155 } 156 return TYPE_TILE; 157 } 158 159 @Override 160 public Holder onCreateViewHolder(ViewGroup parent, int viewType) { 161 final Context context = parent.getContext(); 162 LayoutInflater inflater = LayoutInflater.from(context); 163 if (viewType == TYPE_EDIT) { 164 return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false)); 165 } 166 FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent, 167 false); 168 frame.addView(new QSTileView(context, new QSIconView(context))); 169 return new Holder(frame); 170 } 171 172 @Override 173 public int getItemCount() { 174 return mTiles.size(); 175 } 176 177 @Override 178 public void onBindViewHolder(final Holder holder, final int position) { 179 if (holder.getItemViewType() == TYPE_EDIT) { 180 ((TextView) holder.itemView.findViewById(android.R.id.title)).setText( 181 mCurrentDrag != null ? R.string.drag_to_remove_tiles 182 : R.string.drag_to_add_tiles); 183 return; 184 } 185 if (holder.getItemViewType() == TYPE_ACCESSIBLE_DROP) { 186 holder.mTileView.setClickable(true); 187 holder.mTileView.setFocusable(true); 188 holder.mTileView.setFocusableInTouchMode(true); 189 holder.mTileView.setVisibility(View.VISIBLE); 190 holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 191 holder.mTileView.setContentDescription(mContext.getString( 192 R.string.accessibility_qs_edit_position_label, position + 1)); 193 holder.mTileView.setOnClickListener(new OnClickListener() { 194 @Override 195 public void onClick(View v) { 196 selectPosition(position, v); 197 } 198 }); 199 if (mNeedsFocus) { 200 // Wait for this to get laid out then set its focus. 201 holder.mTileView.addOnLayoutChangeListener(new OnLayoutChangeListener() { 202 @Override 203 public void onLayoutChange(View v, int left, int top, int right, int bottom, 204 int oldLeft, int oldTop, int oldRight, int oldBottom) { 205 holder.mTileView.removeOnLayoutChangeListener(this); 206 holder.mTileView.requestFocus(); 207 } 208 }); 209 mNeedsFocus = false; 210 } 211 return; 212 } 213 214 TileInfo info = mTiles.get(position); 215 216 if (position > mDividerIndex) { 217 info.state.contentDescription = mContext.getString( 218 R.string.accessibility_qs_edit_add_tile_label, info.state.label); 219 } else if (mAccessibilityMoving) { 220 info.state.contentDescription = mContext.getString( 221 R.string.accessibility_qs_edit_position_label, position + 1); 222 } else { 223 info.state.contentDescription = mContext.getString( 224 R.string.accessibility_qs_edit_tile_label, position + 1, info.state.label); 225 } 226 holder.mTileView.onStateChanged(info.state); 227 228 if (mAccessibilityManager.isTouchExplorationEnabled()) { 229 final boolean selectable = !mAccessibilityMoving || position < mDividerIndex; 230 holder.mTileView.setClickable(selectable); 231 holder.mTileView.setFocusable(selectable); 232 holder.mTileView.setImportantForAccessibility(selectable 233 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES 234 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 235 if (selectable) { 236 holder.mTileView.setOnClickListener(new OnClickListener() { 237 @Override 238 public void onClick(View v) { 239 if (mAccessibilityMoving) { 240 selectPosition(position, v); 241 } else { 242 if (position < mDividerIndex) { 243 showAccessibilityDialog(position, v); 244 } else { 245 startAccessibleDrag(position); 246 } 247 } 248 } 249 }); 250 } 251 } 252 } 253 254 private void selectPosition(int position, View v) { 255 // Remove the placeholder. 256 mTiles.remove(mDividerIndex--); 257 mAccessibilityMoving = false; 258 move(mAccessibilityFromIndex, position, v); 259 notifyDataSetChanged(); 260 } 261 262 private void showAccessibilityDialog(final int position, final View v) { 263 TileInfo info = mTiles.get(position); 264 CharSequence[] options = new CharSequence[] { 265 mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label), 266 mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label), 267 }; 268 AlertDialog dialog = new Builder(mContext) 269 .setItems(options, new DialogInterface.OnClickListener() { 270 @Override 271 public void onClick(DialogInterface dialog, int which) { 272 if (which == 0) { 273 startAccessibleDrag(position); 274 } else { 275 move(position, mDividerIndex, v); 276 } 277 } 278 }).setNegativeButton(android.R.string.cancel, null) 279 .create(); 280 SystemUIDialog.setShowForAllUsers(dialog, true); 281 SystemUIDialog.applyFlags(dialog); 282 dialog.show(); 283 } 284 285 private void startAccessibleDrag(int position) { 286 mAccessibilityMoving = true; 287 mNeedsFocus = true; 288 mAccessibilityFromIndex = position; 289 // Add placeholder for last slot. 290 mTiles.add(mDividerIndex++, null); 291 notifyDataSetChanged(); 292 } 293 294 public SpanSizeLookup getSizeLookup() { 295 return mSizeLookup; 296 } 297 298 private boolean move(int from, int to, View v) { 299 if (to > mDividerIndex) { 300 if (from >= mDividerIndex) { 301 return false; 302 } 303 } 304 CharSequence fromLabel = mTiles.get(from).state.label; 305 move(from, to, mTiles); 306 mDividerIndex = mTiles.indexOf(null); 307 notifyItemChanged(from); 308 notifyItemMoved(from, to); 309 CharSequence announcement; 310 if (to >= mDividerIndex) { 311 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE_SPEC, 312 strip(mTiles.get(to))); 313 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_REMOVE, 314 from); 315 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_removed, 316 fromLabel); 317 } else if (from >= mDividerIndex) { 318 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD_SPEC, 319 strip(mTiles.get(to))); 320 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_ADD, 321 to); 322 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_added, 323 fromLabel, (to + 1)); 324 } else { 325 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE_SPEC, 326 strip(mTiles.get(to))); 327 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_QS_EDIT_MOVE, 328 to); 329 announcement = mContext.getString(R.string.accessibility_qs_edit_tile_moved, 330 fromLabel, (to + 1)); 331 } 332 v.announceForAccessibility(announcement); 333 return true; 334 } 335 336 private String strip(TileInfo tileInfo) { 337 String spec = tileInfo.spec; 338 if (spec.startsWith(CustomTile.PREFIX)) { 339 ComponentName component = CustomTile.getComponentFromSpec(spec); 340 return component.getPackageName(); 341 } 342 return spec; 343 } 344 345 private <T> void move(int from, int to, List<T> list) { 346 list.add(from > to ? to : to + 1, list.get(from)); 347 list.remove(from > to ? from + 1 : from); 348 } 349 350 public class Holder extends ViewHolder { 351 private QSTileView mTileView; 352 353 public Holder(View itemView) { 354 super(itemView); 355 if (itemView instanceof FrameLayout) { 356 mTileView = (QSTileView) ((FrameLayout) itemView).getChildAt(0); 357 mTileView.setBackground(null); 358 mTileView.getIcon().disableAnimation(); 359 } 360 } 361 362 public void startDrag() { 363 itemView.animate() 364 .setDuration(DRAG_LENGTH) 365 .scaleX(DRAG_SCALE) 366 .scaleY(DRAG_SCALE); 367 mTileView.findViewById(R.id.tile_label).animate() 368 .setDuration(DRAG_LENGTH) 369 .alpha(0); 370 } 371 372 public void stopDrag() { 373 itemView.animate() 374 .setDuration(DRAG_LENGTH) 375 .scaleX(1) 376 .scaleY(1); 377 mTileView.findViewById(R.id.tile_label).animate() 378 .setDuration(DRAG_LENGTH) 379 .alpha(1); 380 } 381 } 382 383 private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() { 384 @Override 385 public int getSpanSize(int position) { 386 return getItemViewType(position) == TYPE_EDIT ? 3 : 1; 387 } 388 }; 389 390 private final ItemDecoration mDecoration = new ItemDecoration() { 391 // TODO: Move this to resource. 392 private final ColorDrawable mDrawable = new ColorDrawable(0xff384248); 393 394 @Override 395 public void onDraw(Canvas c, RecyclerView parent, State state) { 396 super.onDraw(c, parent, state); 397 398 final int childCount = parent.getChildCount(); 399 final int width = parent.getWidth(); 400 final int bottom = parent.getBottom(); 401 for (int i = 0; i < childCount; i++) { 402 final View child = parent.getChildAt(i); 403 final ViewHolder holder = parent.getChildViewHolder(child); 404 if (holder.getAdapterPosition() < mDividerIndex) { 405 continue; 406 } 407 408 final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 409 .getLayoutParams(); 410 final int top = child.getTop() + params.topMargin + 411 Math.round(ViewCompat.getTranslationY(child)); 412 // Draw full width, in case there aren't tiles all the way across. 413 mDrawable.setBounds(0, top, width, bottom); 414 mDrawable.draw(c); 415 break; 416 } 417 } 418 }; 419 420 private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() { 421 422 @Override 423 public boolean isLongPressDragEnabled() { 424 return true; 425 } 426 427 @Override 428 public boolean isItemViewSwipeEnabled() { 429 return false; 430 } 431 432 @Override 433 public void onSelectedChanged(ViewHolder viewHolder, int actionState) { 434 super.onSelectedChanged(viewHolder, actionState); 435 if (mCurrentDrag != null) { 436 mCurrentDrag.stopDrag(); 437 mCurrentDrag = null; 438 } 439 if (viewHolder != null) { 440 mCurrentDrag = (Holder) viewHolder; 441 mCurrentDrag.startDrag(); 442 } 443 mHandler.post(new Runnable() { 444 @Override 445 public void run() { 446 notifyItemChanged(mDividerIndex); 447 } 448 }); 449 } 450 451 @Override 452 public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) { 453 if (viewHolder.getItemViewType() == TYPE_EDIT) { 454 return makeMovementFlags(0, 0); 455 } 456 int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT 457 | ItemTouchHelper.LEFT; 458 return makeMovementFlags(dragFlags, 0); 459 } 460 461 @Override 462 public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) { 463 int from = viewHolder.getAdapterPosition(); 464 int to = target.getAdapterPosition(); 465 return move(from, to, target.itemView); 466 } 467 468 @Override 469 public void onSwiped(ViewHolder viewHolder, int direction) { 470 } 471 }; 472} 473