1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.documentsui.selection.demo; 18 19 import android.content.Context; 20 import android.os.Bundle; 21 import android.support.annotation.CallSuper; 22 import android.support.v7.app.AppCompatActivity; 23 import android.support.v7.widget.GridLayoutManager; 24 import android.support.v7.widget.RecyclerView; 25 import android.support.v7.widget.Toolbar; 26 import android.view.GestureDetector; 27 import android.view.HapticFeedbackConstants; 28 import android.view.Menu; 29 import android.view.MenuItem; 30 import android.view.MotionEvent; 31 import android.widget.Toast; 32 33 import com.android.documentsui.R; 34 import com.android.documentsui.selection.BandSelectionHelper; 35 import com.android.documentsui.selection.ContentLock; 36 import com.android.documentsui.selection.DefaultBandHost; 37 import com.android.documentsui.selection.DefaultBandPredicate; 38 import com.android.documentsui.selection.DefaultSelectionHelper; 39 import com.android.documentsui.selection.GestureRouter; 40 import com.android.documentsui.selection.GestureSelectionHelper; 41 import com.android.documentsui.selection.ItemDetailsLookup; 42 import com.android.documentsui.selection.ItemDetailsLookup.ItemDetails; 43 import com.android.documentsui.selection.MotionInputHandler; 44 import com.android.documentsui.selection.MouseInputHandler; 45 import com.android.documentsui.selection.MutableSelection; 46 import com.android.documentsui.selection.Selection; 47 import com.android.documentsui.selection.SelectionHelper; 48 import com.android.documentsui.selection.SelectionHelper.SelectionPredicate; 49 import com.android.documentsui.selection.SelectionHelper.StableIdProvider; 50 import com.android.documentsui.selection.TouchEventRouter; 51 import com.android.documentsui.selection.TouchInputHandler; 52 import com.android.documentsui.selection.demo.SelectionDemoAdapter.OnBindCallback; 53 54 /** 55 * ContentPager demo activity. 56 */ 57 public class SelectionDemoActivity extends AppCompatActivity { 58 59 private static final String EXTRA_SAVED_SELECTION = "demo-saved-selection"; 60 private static final String EXTRA_COLUMN_COUNT = "demo-column-count"; 61 62 private Toolbar mToolbar; 63 private SelectionDemoAdapter mAdapter; 64 private SelectionHelper mSelectionHelper; 65 66 private RecyclerView mRecView; 67 private GridLayoutManager mLayout; 68 private int mColumnCount = 1; // This will get updated when layout changes. 69 70 @Override onCreate(Bundle savedInstanceState)71 protected void onCreate(Bundle savedInstanceState) { 72 super.onCreate(savedInstanceState); 73 74 setContentView(R.layout.selection_demo_layout); 75 mToolbar = findViewById(R.id.toolbar); 76 setSupportActionBar(mToolbar); 77 mRecView = (RecyclerView) findViewById(R.id.list); 78 79 mLayout = new GridLayoutManager(this, mColumnCount); 80 mRecView.setLayoutManager(mLayout); 81 82 mAdapter = new SelectionDemoAdapter(this); 83 mRecView.setAdapter(mAdapter); 84 85 StableIdProvider stableIds = new DemoStableIdProvider(mAdapter); 86 87 // SelectionPredicate permits client control of which items can be selected. 88 SelectionPredicate canSelectAnything = new SelectionPredicate() { 89 @Override 90 public boolean canSetStateForId(String id, boolean nextState) { 91 return true; 92 } 93 94 @Override 95 public boolean canSetStateAtPosition(int position, boolean nextState) { 96 return true; 97 } 98 }; 99 100 // TODO: Reload content when it changes. Could use CursorLoader. 101 // TODO: Retain selection. Restore when content changes. 102 103 mSelectionHelper = new DefaultSelectionHelper( 104 DefaultSelectionHelper.MODE_MULTIPLE, 105 mAdapter, 106 stableIds, 107 canSelectAnything); 108 109 // onBind event callback that allows items to be updated to reflect 110 // selection status when bound by recycler view. 111 // This allows us to defer initialization of the SelectionHelper dependency 112 // which itself depends on the Adapter. 113 mAdapter.addOnBindCallback( 114 new OnBindCallback() { 115 @Override 116 void onBound(DemoHolder holder, int position) { 117 String id = mAdapter.getStableId(position); 118 holder.setSelected(mSelectionHelper.isSelected(id)); 119 } 120 }); 121 122 ItemDetailsLookup detailsLookup = new DemoDetailsLookup(mRecView); 123 124 // Setup basic input handling, with the touch handler as the default consumer 125 // of events. If mouse handling is configured as well, the mouse input 126 // related handlers will intercept mouse input events. 127 128 // GestureRouter is responsible for routing GestureDetector events 129 // to tool-type specific handlers. 130 GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>(); 131 GestureDetector gestureDetector = new GestureDetector(this, gestureRouter); 132 133 // TouchEventRouter takes its name from RecyclerView#OnItemTouchListener. 134 // Despite "Touch" being in the name, it receives events for all types of tools. 135 // This class is responsible for routing events to tool-type specific handlers, 136 // and if not handled by a handler, on to a GestureDetector for analysis. 137 TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector); 138 139 // Content lock provides a mechanism to block content reload while selection 140 // activities are active. If using a loader to load content, route 141 // the call through the content lock using ContentLock#runWhenUnlocked. 142 // This is especially useful when listening on content change notification. 143 ContentLock contentLock = new ContentLock(); 144 145 // GestureSelectionHelper provides logic that interprets a combination 146 // of motions and gestures in order to provide gesture driven selection support 147 // when used in conjunction with RecyclerView. 148 GestureSelectionHelper gestureHelper = GestureSelectionHelper.create( 149 mSelectionHelper, mRecView, contentLock, detailsLookup); 150 151 // Finally hook the framework up to listening to recycle view events. 152 mRecView.addOnItemTouchListener(eventRouter); 153 154 // But before you move on, there's more work to do. Event plumbing has been 155 // installed, but we haven't registered any of our helpers or callbacks. 156 // Helpers contain predefined logic converting events into selection related events. 157 // Callbacks provide authors the ability to reponspond to other types of 158 // events (like "active" a tapped item). This is broken up into two main 159 // suites, one for "touch" and one for "mouse", though both can and should (usually) 160 // be configued to handle other types of input (to satisfy user expectation). 161 162 // TOUCH (+ UNKNOWN) handeling provides gesture based selection allowing 163 // the user to long press on an item, then drag her finger over other 164 // items in order to extend the selection. 165 TouchCallbacks touchCallbacks = new TouchCallbacks(this, mRecView); 166 167 // Provides high level glue for binding touch events and gestures to selection framework. 168 TouchInputHandler touchHandler = new TouchInputHandler( 169 mSelectionHelper, detailsLookup, canSelectAnything, gestureHelper, touchCallbacks); 170 171 eventRouter.register(MotionEvent.TOOL_TYPE_FINGER, gestureHelper); 172 eventRouter.register(MotionEvent.TOOL_TYPE_UNKNOWN, gestureHelper); 173 174 gestureRouter.register(MotionEvent.TOOL_TYPE_FINGER, touchHandler); 175 gestureRouter.register(MotionEvent.TOOL_TYPE_UNKNOWN, touchHandler); 176 177 // MOUSE (+ STYLUS) handeling provides band based selection allowing 178 // the user to click down in an empty area, then drag her mouse 179 // to create a band that covers the items she wants selected. 180 // 181 // PRO TIP: Don't skip installing mouse/stylus support. It provides 182 // improved productivity and demonstrates feature maturity that users 183 // will appreciate. See InputManager for details on more sophisticated 184 // strategies on detecting the presence of input tools. 185 186 // Provides high level glue for binding mouse/stylus events and gestures 187 // to selection framework. 188 MouseInputHandler mouseHandler = new MouseInputHandler( 189 mSelectionHelper, detailsLookup, new MouseCallbacks(this, mRecView)); 190 191 DefaultBandHost host = new DefaultBandHost( 192 mRecView, R.drawable.selection_demo_band_overlay); 193 194 // BandSelectionHelper provides support for band selection on-top of a RecyclerView 195 // instance. Given the recycling nature of RecyclerView BandSelectionController 196 // necessarily models and caches list/grid information as the user's pointer 197 // interacts with the item in the RecyclerView. Selectable items that intersect 198 // with the band, both on and off screen, are selected. 199 BandSelectionHelper bandHelper = new BandSelectionHelper( 200 host, 201 mAdapter, 202 stableIds, 203 mSelectionHelper, 204 canSelectAnything, 205 new DefaultBandPredicate(detailsLookup), 206 contentLock); 207 208 209 eventRouter.register(MotionEvent.TOOL_TYPE_MOUSE, bandHelper); 210 eventRouter.register(MotionEvent.TOOL_TYPE_STYLUS, bandHelper); 211 212 gestureRouter.register(MotionEvent.TOOL_TYPE_MOUSE, mouseHandler); 213 gestureRouter.register(MotionEvent.TOOL_TYPE_STYLUS, mouseHandler); 214 215 // Aaaaan, all done with mouse/stylus selection setup! 216 217 updateFromSavedState(savedInstanceState); 218 } 219 220 @Override onSaveInstanceState(Bundle state)221 protected void onSaveInstanceState(Bundle state) { 222 super.onSaveInstanceState(state); 223 MutableSelection selection = new MutableSelection(); 224 mSelectionHelper.copySelection(selection); 225 state.putParcelable(EXTRA_SAVED_SELECTION, selection); 226 state.putInt(EXTRA_COLUMN_COUNT, mColumnCount); 227 } 228 updateFromSavedState(Bundle state)229 private void updateFromSavedState(Bundle state) { 230 // In order to preserve selection across various lifecycle events be sure to save 231 // the selection in onSaveInstanceState, and to restore it when present in the Bundle 232 // pass in via onCreate(Bundle). 233 if (state != null) { 234 if (state.containsKey(EXTRA_SAVED_SELECTION)) { 235 Selection savedSelection = state.getParcelable(EXTRA_SAVED_SELECTION); 236 if (!savedSelection.isEmpty()) { 237 mSelectionHelper.restoreSelection(savedSelection); 238 CharSequence text = "Selection restored."; 239 Toast.makeText(this, "Selection restored.", Toast.LENGTH_SHORT).show(); 240 } 241 } 242 if (state.containsKey(EXTRA_COLUMN_COUNT)) { 243 mColumnCount = state.getInt(EXTRA_COLUMN_COUNT); 244 mLayout.setSpanCount(mColumnCount); 245 } 246 } 247 } 248 249 @Override onCreateOptionsMenu(Menu menu)250 public boolean onCreateOptionsMenu(Menu menu) { 251 boolean showMenu = super.onCreateOptionsMenu(menu); 252 getMenuInflater().inflate(R.menu.selection_demo_actions, menu); 253 return showMenu; 254 } 255 256 @Override 257 @CallSuper onPrepareOptionsMenu(Menu menu)258 public boolean onPrepareOptionsMenu(Menu menu) { 259 super.onPrepareOptionsMenu(menu); 260 menu.findItem(R.id.option_menu_add_column).setEnabled(mColumnCount <= 3); 261 menu.findItem(R.id.option_menu_remove_column).setEnabled(mColumnCount > 1); 262 return true; 263 } 264 265 @Override onOptionsItemSelected(MenuItem item)266 public boolean onOptionsItemSelected(MenuItem item) { 267 switch (item.getItemId()) { 268 case R.id.option_menu_add_column: 269 // TODO: Add columns 270 mLayout.setSpanCount(++mColumnCount); 271 return true; 272 273 case R.id.option_menu_remove_column: 274 mLayout.setSpanCount(--mColumnCount); 275 return true; 276 default: 277 return super.onOptionsItemSelected(item); 278 } 279 } 280 281 282 @Override onBackPressed()283 public void onBackPressed () { 284 if (mSelectionHelper.hasSelection()) { 285 mSelectionHelper.clearSelection(); 286 mSelectionHelper.clearProvisionalSelection(); 287 } else { 288 super.onBackPressed(); 289 } 290 } 291 toast(Context context, String msg)292 private static void toast(Context context, String msg) { 293 Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); 294 } 295 296 @Override onDestroy()297 protected void onDestroy() { 298 mSelectionHelper.clearSelection(); 299 super.onDestroy(); 300 } 301 302 @Override onStart()303 protected void onStart() { 304 super.onStart(); 305 mAdapter.loadData(); 306 } 307 308 // Implementation of MouseInputHandler.Callbacks allows handling 309 // of higher level events, like onActivated. 310 private static final class MouseCallbacks extends MouseInputHandler.Callbacks { 311 312 private final Context mContext; 313 private final RecyclerView mRecView; 314 MouseCallbacks(Context context, RecyclerView recView)315 MouseCallbacks(Context context, RecyclerView recView) { 316 mContext = context; 317 mRecView = recView; 318 } 319 320 @Override onItemActivated(ItemDetails item, MotionEvent e)321 public boolean onItemActivated(ItemDetails item, MotionEvent e) { 322 toast(mContext, "Activate item: " + item.getStableId()); 323 return true; 324 } 325 326 @Override onContextClick(MotionEvent e)327 public boolean onContextClick(MotionEvent e) { 328 toast(mContext, "Context click received."); 329 return true; 330 } 331 332 @Override onPerformHapticFeedback()333 public void onPerformHapticFeedback() { 334 mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 335 } 336 }; 337 338 private static final class TouchCallbacks extends TouchInputHandler.Callbacks { 339 340 private final Context mContext; 341 private final RecyclerView mRecView; 342 TouchCallbacks(Context context, RecyclerView recView)343 private TouchCallbacks(Context context, RecyclerView recView) { 344 345 mContext = context; 346 mRecView = recView; 347 } 348 349 @Override onItemActivated(ItemDetails item, MotionEvent e)350 public boolean onItemActivated(ItemDetails item, MotionEvent e) { 351 toast(mContext, "Activate item: " + item.getStableId()); 352 return true; 353 } 354 355 @Override onPerformHapticFeedback()356 public void onPerformHapticFeedback() { 357 mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 358 } 359 } 360 }