Folder.java revision e4f9891f01bdc10d8f96e4e2429e2f4d0558238b
1/* 2 * Copyright (C) 2008 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 17package com.android.launcher3; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.animation.PropertyValuesHolder; 23import android.content.Context; 24import android.content.res.Resources; 25import android.graphics.PointF; 26import android.graphics.Rect; 27import android.graphics.drawable.Drawable; 28import android.text.InputType; 29import android.text.Selection; 30import android.text.Spannable; 31import android.util.AttributeSet; 32import android.util.Log; 33import android.view.ActionMode; 34import android.view.KeyEvent; 35import android.view.LayoutInflater; 36import android.view.Menu; 37import android.view.MenuItem; 38import android.view.MotionEvent; 39import android.view.View; 40import android.view.accessibility.AccessibilityEvent; 41import android.view.accessibility.AccessibilityManager; 42import android.view.animation.AccelerateInterpolator; 43import android.view.animation.Interpolator; 44import android.view.inputmethod.EditorInfo; 45import android.view.inputmethod.InputMethodManager; 46import android.widget.LinearLayout; 47import android.widget.ScrollView; 48import android.widget.TextView; 49 50import com.android.launcher3.FolderInfo.FolderListener; 51 52import java.util.ArrayList; 53import java.util.Collections; 54import java.util.Comparator; 55 56/** 57 * Represents a set of icons chosen by the user or generated by the system. 58 */ 59public class Folder extends LinearLayout implements DragSource, View.OnClickListener, 60 View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener, 61 View.OnFocusChangeListener { 62 private static final String TAG = "Launcher.Folder"; 63 64 protected DragController mDragController; 65 protected Launcher mLauncher; 66 protected FolderInfo mInfo; 67 68 static final int STATE_NONE = -1; 69 static final int STATE_SMALL = 0; 70 static final int STATE_ANIMATING = 1; 71 static final int STATE_OPEN = 2; 72 73 private int mExpandDuration; 74 protected CellLayout mContent; 75 private ScrollView mScrollView; 76 private final LayoutInflater mInflater; 77 private final IconCache mIconCache; 78 private int mState = STATE_NONE; 79 private static final int REORDER_ANIMATION_DURATION = 230; 80 private static final int REORDER_DELAY = 250; 81 private static final int ON_EXIT_CLOSE_DELAY = 800; 82 private boolean mRearrangeOnClose = false; 83 private FolderIcon mFolderIcon; 84 private int mMaxCountX; 85 private int mMaxCountY; 86 private int mMaxVisibleX; 87 private int mMaxVisibleY; 88 private int mMaxContentAreaHeight = 0; 89 private int mMaxNumItems; 90 private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>(); 91 private Drawable mIconDrawable; 92 boolean mItemsInvalidated = false; 93 private ShortcutInfo mCurrentDragInfo; 94 private View mCurrentDragView; 95 boolean mSuppressOnAdd = false; 96 private int[] mTargetCell = new int[2]; 97 private int[] mPreviousTargetCell = new int[2]; 98 private int[] mEmptyCell = new int[2]; 99 private Alarm mReorderAlarm = new Alarm(); 100 private Alarm mOnExitAlarm = new Alarm(); 101 private int mFolderNameHeight; 102 private Rect mTempRect = new Rect(); 103 private boolean mDragInProgress = false; 104 private boolean mDeleteFolderOnDropCompleted = false; 105 private boolean mSuppressFolderDeletion = false; 106 private boolean mItemAddedBackToSelfViaIcon = false; 107 FolderEditText mFolderName; 108 private float mFolderIconPivotX; 109 private float mFolderIconPivotY; 110 private long mDragScrollStart; 111 112 /** Interpolator used to scale velocity with touch position, may be null. */ 113 private Interpolator mEdgeInterpolator = new AccelerateInterpolator(); 114 115 private static final int SCROLL_CUT_OFF_AMOUNT = 60; 116 117 private final float mDurationToMax = 300f; 118 private final float mMaxVelocity = 100000f/1000f; 119 120 // Aim for this amount of target area to trigger scrolling 121 private static float sScrollBandFactor = -1f; 122 private static float sScrollBandMaxFactor = -1f; 123 124 // Min size for the target area to trigger scrolling 125 private static int sMinScrollBandHeight = -1; 126 127 private boolean mIsEditingName = false; 128 private InputMethodManager mInputMethodManager; 129 130 private static String sDefaultFolderName; 131 private static String sHintText; 132 133 private int DRAG_MODE_NONE = 0; 134 private int DRAG_MODE_REORDER = 1; 135 private int DRAG_MODE_SCROLL_UP = 2; 136 private int DRAG_MODE_SCROLL_DOWN = 3; 137 private int mDragMode = DRAG_MODE_NONE; 138 139 private boolean mDestroyed; 140 141 /** 142 * Used to inflate the Workspace from XML. 143 * 144 * @param context The application's context. 145 * @param attrs The attribtues set containing the Workspace's customization values. 146 */ 147 public Folder(Context context, AttributeSet attrs) { 148 super(context, attrs); 149 setAlwaysDrawnWithCacheEnabled(false); 150 mInflater = LayoutInflater.from(context); 151 mIconCache = (LauncherAppState.getInstance()).getIconCache(); 152 153 Resources res = getResources(); 154 mMaxCountX = mMaxVisibleX = res.getInteger(R.integer.folder_max_count_x); 155 mMaxCountY = mMaxVisibleY = res.getInteger(R.integer.folder_max_count_y); 156 mMaxNumItems = res.getInteger(R.integer.folder_max_num_items); 157 158 if (mMaxCountY == -1) { 159 // -2 indicates unlimited 160 mMaxCountY = Integer.MAX_VALUE; 161 mMaxVisibleX = LauncherModel.getCellCountX() + 1; 162 } 163 if (mMaxNumItems == -1) { 164 // -2 indicates unlimited 165 mMaxNumItems = Integer.MAX_VALUE; 166 mMaxVisibleY = LauncherModel.getCellCountY() + 1; 167 } 168 if (mMaxCountX == 0) { 169 mMaxCountX = mMaxVisibleX = LauncherModel.getCellCountX(); 170 mMaxVisibleX++; 171 } 172 if (mMaxCountY == 0) { 173 mMaxCountY = mMaxVisibleY = LauncherModel.getCellCountY(); 174 mMaxVisibleY++; 175 } 176 if (mMaxNumItems == 0) { 177 mMaxNumItems = mMaxCountX * mMaxCountY; 178 if (mMaxNumItems < 0) { 179 mMaxNumItems = Integer.MAX_VALUE; 180 } 181 } 182 183 mInputMethodManager = (InputMethodManager) 184 getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 185 186 mExpandDuration = res.getInteger(R.integer.config_folderAnimDuration); 187 188 if (sDefaultFolderName == null) { 189 sDefaultFolderName = res.getString(R.string.folder_name); 190 } 191 if (sHintText == null) { 192 sHintText = res.getString(R.string.folder_hint_text); 193 } 194 if (sMinScrollBandHeight == -1) { 195 sMinScrollBandHeight = res.getDimensionPixelSize(R.dimen.min_scroll_band_size); 196 } 197 if (sScrollBandFactor == -1f) { 198 sScrollBandFactor = res.getInteger(R.integer.scroll_band_factor)/100f; 199 } 200 if (sScrollBandMaxFactor == -1f) { 201 sScrollBandMaxFactor = res.getInteger(R.integer.scroll_band_max_factor)/100f; 202 } 203 mLauncher = (Launcher) context; 204 // We need this view to be focusable in touch mode so that when text editing of the folder 205 // name is complete, we have something to focus on, thus hiding the cursor and giving 206 // reliable behvior when clicking the text field (since it will always gain focus on click). 207 setFocusableInTouchMode(true); 208 } 209 210 @Override 211 protected void onFinishInflate() { 212 super.onFinishInflate(); 213 mScrollView = (ScrollView) findViewById(R.id.scroll_view); 214 mContent = (CellLayout) findViewById(R.id.folder_content); 215 216 // Beyond this height, the area scrolls 217 mContent.setGridSize(mMaxVisibleX, mMaxVisibleY); 218 mMaxContentAreaHeight = mContent.getDesiredHeight() - SCROLL_CUT_OFF_AMOUNT; 219 220 mContent.setGridSize(0, 0); 221 mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false); 222 mContent.setInvertIfRtl(true); 223 mFolderName = (FolderEditText) findViewById(R.id.folder_name); 224 mFolderName.setFolder(this); 225 mFolderName.setOnFocusChangeListener(this); 226 227 // We find out how tall the text view wants to be (it is set to wrap_content), so that 228 // we can allocate the appropriate amount of space for it. 229 int measureSpec = MeasureSpec.UNSPECIFIED; 230 mFolderName.measure(measureSpec, measureSpec); 231 mFolderNameHeight = mFolderName.getMeasuredHeight(); 232 233 // We disable action mode for now since it messes up the view on phones 234 mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback); 235 mFolderName.setOnEditorActionListener(this); 236 mFolderName.setSelectAllOnFocus(true); 237 mFolderName.setInputType(mFolderName.getInputType() | 238 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS); 239 } 240 241 private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { 242 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 243 return false; 244 } 245 246 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 247 return false; 248 } 249 250 public void onDestroyActionMode(ActionMode mode) { 251 } 252 253 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 254 return false; 255 } 256 }; 257 258 public void onClick(View v) { 259 Object tag = v.getTag(); 260 if (tag instanceof ShortcutInfo) { 261 mLauncher.onClick(v); 262 } 263 } 264 265 public boolean onLongClick(View v) { 266 // Return if global dragging is not enabled 267 if (!mLauncher.isDraggingEnabled()) return true; 268 269 Object tag = v.getTag(); 270 if (tag instanceof ShortcutInfo) { 271 ShortcutInfo item = (ShortcutInfo) tag; 272 if (!v.isInTouchMode()) { 273 return false; 274 } 275 276 mLauncher.dismissFolderCling(null); 277 278 mLauncher.getWorkspace().onDragStartedWithItem(v); 279 mLauncher.getWorkspace().beginDragShared(v, this); 280 mIconDrawable = ((TextView) v).getCompoundDrawables()[1]; 281 282 mCurrentDragInfo = item; 283 mEmptyCell[0] = item.cellX; 284 mEmptyCell[1] = item.cellY; 285 mCurrentDragView = v; 286 287 mContent.removeView(mCurrentDragView); 288 mInfo.remove(mCurrentDragInfo); 289 mDragInProgress = true; 290 mItemAddedBackToSelfViaIcon = false; 291 } 292 return true; 293 } 294 295 public boolean isEditingName() { 296 return mIsEditingName; 297 } 298 299 public void startEditingFolderName() { 300 mFolderName.setHint(""); 301 mIsEditingName = true; 302 } 303 304 public void dismissEditingName() { 305 mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); 306 doneEditingFolderName(true); 307 } 308 309 public void doneEditingFolderName(boolean commit) { 310 mFolderName.setHint(sHintText); 311 // Convert to a string here to ensure that no other state associated with the text field 312 // gets saved. 313 String newTitle = mFolderName.getText().toString(); 314 mInfo.setTitle(newTitle); 315 LauncherModel.updateItemInDatabase(mLauncher, mInfo); 316 317 if (commit) { 318 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 319 String.format(getContext().getString(R.string.folder_renamed), newTitle)); 320 } 321 // In order to clear the focus from the text field, we set the focus on ourself. This 322 // ensures that every time the field is clicked, focus is gained, giving reliable behavior. 323 requestFocus(); 324 325 Selection.setSelection((Spannable) mFolderName.getText(), 0, 0); 326 mIsEditingName = false; 327 } 328 329 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 330 if (actionId == EditorInfo.IME_ACTION_DONE) { 331 dismissEditingName(); 332 return true; 333 } 334 return false; 335 } 336 337 public View getEditTextRegion() { 338 return mFolderName; 339 } 340 341 public Drawable getDragDrawable() { 342 return mIconDrawable; 343 } 344 345 /** 346 * We need to handle touch events to prevent them from falling through to the workspace below. 347 */ 348 @Override 349 public boolean onTouchEvent(MotionEvent ev) { 350 return true; 351 } 352 353 public void setDragController(DragController dragController) { 354 mDragController = dragController; 355 } 356 357 void setFolderIcon(FolderIcon icon) { 358 mFolderIcon = icon; 359 } 360 361 @Override 362 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 363 // When the folder gets focus, we don't want to announce the list of items. 364 return true; 365 } 366 367 /** 368 * @return the FolderInfo object associated with this folder 369 */ 370 FolderInfo getInfo() { 371 return mInfo; 372 } 373 374 private class GridComparator implements Comparator<ShortcutInfo> { 375 int mNumCols; 376 public GridComparator(int numCols) { 377 mNumCols = numCols; 378 } 379 380 @Override 381 public int compare(ShortcutInfo lhs, ShortcutInfo rhs) { 382 int lhIndex = lhs.cellY * mNumCols + lhs.cellX; 383 int rhIndex = rhs.cellY * mNumCols + rhs.cellX; 384 return (lhIndex - rhIndex); 385 } 386 } 387 388 private void placeInReadingOrder(ArrayList<ShortcutInfo> items) { 389 int maxX = 0; 390 int count = items.size(); 391 for (int i = 0; i < count; i++) { 392 ShortcutInfo item = items.get(i); 393 if (item.cellX > maxX) { 394 maxX = item.cellX; 395 } 396 } 397 398 GridComparator gridComparator = new GridComparator(maxX + 1); 399 Collections.sort(items, gridComparator); 400 final int countX = mContent.getCountX(); 401 for (int i = 0; i < count; i++) { 402 int x = i % countX; 403 int y = i / countX; 404 ShortcutInfo item = items.get(i); 405 item.cellX = x; 406 item.cellY = y; 407 } 408 } 409 410 void bind(FolderInfo info) { 411 mInfo = info; 412 ArrayList<ShortcutInfo> children = info.contents; 413 ArrayList<ShortcutInfo> overflow = new ArrayList<ShortcutInfo>(); 414 setupContentForNumItems(children.size()); 415 placeInReadingOrder(children); 416 int count = 0; 417 for (int i = 0; i < children.size(); i++) { 418 ShortcutInfo child = (ShortcutInfo) children.get(i); 419 if (!createAndAddShortcut(child)) { 420 overflow.add(child); 421 } else { 422 count++; 423 } 424 } 425 426 // We rearrange the items in case there are any empty gaps 427 setupContentForNumItems(count); 428 429 // If our folder has too many items we prune them from the list. This is an issue 430 // when upgrading from the old Folders implementation which could contain an unlimited 431 // number of items. 432 for (ShortcutInfo item: overflow) { 433 mInfo.remove(item); 434 LauncherModel.deleteItemFromDatabase(mLauncher, item); 435 } 436 437 mItemsInvalidated = true; 438 updateTextViewFocus(); 439 mInfo.addListener(this); 440 441 if (!sDefaultFolderName.contentEquals(mInfo.title)) { 442 mFolderName.setText(mInfo.title); 443 } else { 444 mFolderName.setText(""); 445 } 446 updateItemLocationsInDatabase(); 447 } 448 449 /** 450 * Creates a new UserFolder, inflated from R.layout.user_folder. 451 * 452 * @param context The application's context. 453 * 454 * @return A new UserFolder. 455 */ 456 static Folder fromXml(Context context) { 457 return (Folder) LayoutInflater.from(context).inflate(R.layout.user_folder, null); 458 } 459 460 /** 461 * This method is intended to make the UserFolder to be visually identical in size and position 462 * to its associated FolderIcon. This allows for a seamless transition into the expanded state. 463 */ 464 private void positionAndSizeAsIcon() { 465 if (!(getParent() instanceof DragLayer)) return; 466 setScaleX(0.8f); 467 setScaleY(0.8f); 468 setAlpha(0f); 469 mState = STATE_SMALL; 470 } 471 472 public void animateOpen() { 473 positionAndSizeAsIcon(); 474 475 if (!(getParent() instanceof DragLayer)) return; 476 centerAboutIcon(); 477 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1); 478 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f); 479 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f); 480 final ObjectAnimator oa = 481 LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); 482 483 oa.addListener(new AnimatorListenerAdapter() { 484 @Override 485 public void onAnimationStart(Animator animation) { 486 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 487 String.format(getContext().getString(R.string.folder_opened), 488 mContent.getCountX(), mContent.getCountY())); 489 mState = STATE_ANIMATING; 490 } 491 @Override 492 public void onAnimationEnd(Animator animation) { 493 mState = STATE_OPEN; 494 setLayerType(LAYER_TYPE_NONE, null); 495 Cling cling = mLauncher.showFirstRunFoldersCling(); 496 if (cling != null) { 497 cling.bringToFront(); 498 } 499 setFocusOnFirstChild(); 500 } 501 }); 502 oa.setDuration(mExpandDuration); 503 setLayerType(LAYER_TYPE_HARDWARE, null); 504 oa.start(); 505 } 506 507 private void sendCustomAccessibilityEvent(int type, String text) { 508 AccessibilityManager accessibilityManager = (AccessibilityManager) 509 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 510 if (accessibilityManager.isEnabled()) { 511 AccessibilityEvent event = AccessibilityEvent.obtain(type); 512 onInitializeAccessibilityEvent(event); 513 event.getText().add(text); 514 accessibilityManager.sendAccessibilityEvent(event); 515 } 516 } 517 518 private void setFocusOnFirstChild() { 519 View firstChild = mContent.getChildAt(0, 0); 520 if (firstChild != null) { 521 firstChild.requestFocus(); 522 } 523 } 524 525 public void animateClosed() { 526 if (!(getParent() instanceof DragLayer)) return; 527 PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0); 528 PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 0.9f); 529 PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 0.9f); 530 final ObjectAnimator oa = 531 LauncherAnimUtils.ofPropertyValuesHolder(this, alpha, scaleX, scaleY); 532 533 oa.addListener(new AnimatorListenerAdapter() { 534 @Override 535 public void onAnimationEnd(Animator animation) { 536 onCloseComplete(); 537 setLayerType(LAYER_TYPE_NONE, null); 538 mState = STATE_SMALL; 539 } 540 @Override 541 public void onAnimationStart(Animator animation) { 542 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, 543 getContext().getString(R.string.folder_closed)); 544 mState = STATE_ANIMATING; 545 } 546 }); 547 oa.setDuration(mExpandDuration); 548 setLayerType(LAYER_TYPE_HARDWARE, null); 549 oa.start(); 550 } 551 552 void notifyDataSetChanged() { 553 // recreate all the children if the data set changes under us. We may want to do this more 554 // intelligently (ie just removing the views that should no longer exist) 555 mContent.removeAllViewsInLayout(); 556 bind(mInfo); 557 } 558 559 public boolean acceptDrop(DragObject d) { 560 final ItemInfo item = (ItemInfo) d.dragInfo; 561 final int itemType = item.itemType; 562 return ((itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION || 563 itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) && 564 !isFull()); 565 } 566 567 protected boolean findAndSetEmptyCells(ShortcutInfo item) { 568 int[] emptyCell = new int[2]; 569 if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) { 570 item.cellX = emptyCell[0]; 571 item.cellY = emptyCell[1]; 572 return true; 573 } else { 574 return false; 575 } 576 } 577 578 protected boolean createAndAddShortcut(ShortcutInfo item) { 579 final TextView textView = 580 (TextView) mInflater.inflate(R.layout.application, this, false); 581 textView.setCompoundDrawablesWithIntrinsicBounds(null, 582 new FastBitmapDrawable(item.getIcon(mIconCache)), null, null); 583 textView.setText(item.title); 584 textView.setTag(item); 585 586 textView.setOnClickListener(this); 587 textView.setOnLongClickListener(this); 588 589 // We need to check here to verify that the given item's location isn't already occupied 590 // by another item. 591 if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0 592 || item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) { 593 // This shouldn't happen, log it. 594 Log.e(TAG, "Folder order not properly persisted during bind"); 595 if (!findAndSetEmptyCells(item)) { 596 return false; 597 } 598 } 599 600 CellLayout.LayoutParams lp = 601 new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY); 602 boolean insert = false; 603 textView.setOnKeyListener(new FolderKeyEventListener()); 604 mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true); 605 return true; 606 } 607 608 public void onDragEnter(DragObject d) { 609 mPreviousTargetCell[0] = -1; 610 mPreviousTargetCell[1] = -1; 611 mOnExitAlarm.cancelAlarm(); 612 } 613 614 OnAlarmListener mReorderAlarmListener = new OnAlarmListener() { 615 public void onAlarm(Alarm alarm) { 616 realTimeReorder(mEmptyCell, mTargetCell); 617 } 618 }; 619 620 boolean readingOrderGreaterThan(int[] v1, int[] v2) { 621 if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) { 622 return true; 623 } else { 624 return false; 625 } 626 } 627 628 private void realTimeReorder(int[] empty, int[] target) { 629 boolean wrap; 630 int startX; 631 int endX; 632 int startY; 633 int delay = 0; 634 float delayAmount = 30; 635 if (readingOrderGreaterThan(target, empty)) { 636 wrap = empty[0] >= mContent.getCountX() - 1; 637 startY = wrap ? empty[1] + 1 : empty[1]; 638 for (int y = startY; y <= target[1]; y++) { 639 startX = y == empty[1] ? empty[0] + 1 : 0; 640 endX = y < target[1] ? mContent.getCountX() - 1 : target[0]; 641 for (int x = startX; x <= endX; x++) { 642 View v = mContent.getChildAt(x,y); 643 if (mContent.animateChildToPosition(v, empty[0], empty[1], 644 REORDER_ANIMATION_DURATION, delay, true, true)) { 645 empty[0] = x; 646 empty[1] = y; 647 delay += delayAmount; 648 delayAmount *= 0.9; 649 } 650 } 651 } 652 } else { 653 wrap = empty[0] == 0; 654 startY = wrap ? empty[1] - 1 : empty[1]; 655 for (int y = startY; y >= target[1]; y--) { 656 startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1; 657 endX = y > target[1] ? 0 : target[0]; 658 for (int x = startX; x >= endX; x--) { 659 View v = mContent.getChildAt(x,y); 660 if (mContent.animateChildToPosition(v, empty[0], empty[1], 661 REORDER_ANIMATION_DURATION, delay, true, true)) { 662 empty[0] = x; 663 empty[1] = y; 664 delay += delayAmount; 665 delayAmount *= 0.9; 666 } 667 } 668 } 669 } 670 } 671 672 public boolean isLayoutRtl() { 673 return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); 674 } 675 676 public void onDragOver(DragObject d) { 677 int scrollOffset = mScrollView.getScrollY(); 678 int height = getMeasuredHeight(); 679 int scrollBandHeight = Math.min((int) (height * sScrollBandMaxFactor), 680 (Math.max(sMinScrollBandHeight, (int) (height * sScrollBandFactor)))); 681 float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView, null); 682 r[0] -= getPaddingLeft(); 683 r[1] -= getPaddingTop(); 684 685 mTargetCell = mContent.findNearestArea((int) r[0], (int) r[1] + scrollOffset, 686 1, 1, mTargetCell); 687 688 if (isLayoutRtl()) { 689 mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1; 690 } 691 692 if (r[1] < scrollBandHeight && mScrollView.getScrollY() > 0) { 693 // Scroll up 694 if (mDragMode != DRAG_MODE_SCROLL_UP) { 695 mDragMode = DRAG_MODE_SCROLL_UP; 696 mDragScrollStart = System.currentTimeMillis(); 697 scrollUp(); 698 } 699 mReorderAlarm.cancelAlarm(); 700 } else if (r[1] > (getFolderHeight() - scrollBandHeight) && mScrollView.getScrollY() < 701 (mContent.getMeasuredHeight() - mScrollView.getMeasuredHeight())) { 702 if (mDragMode != DRAG_MODE_SCROLL_DOWN) { 703 mDragMode = DRAG_MODE_SCROLL_DOWN; 704 mDragScrollStart = System.currentTimeMillis(); 705 scrollDown(); 706 } 707 mReorderAlarm.cancelAlarm(); 708 } else if (mTargetCell[0] != mPreviousTargetCell[0] || mTargetCell[1] != mPreviousTargetCell[1]) { 709 mReorderAlarm.cancelAlarm(); 710 mReorderAlarm.setOnAlarmListener(mReorderAlarmListener); 711 mReorderAlarm.setAlarm(REORDER_DELAY); 712 mPreviousTargetCell[0] = mTargetCell[0]; 713 mPreviousTargetCell[1] = mTargetCell[1]; 714 mDragMode = DRAG_MODE_REORDER; 715 } else { 716 mDragMode = DRAG_MODE_NONE; 717 } 718 } 719 720 Runnable mScrollUpRunnable = new Runnable() { 721 @Override 722 public void run() { 723 scrollUp(); 724 } 725 }; 726 727 Runnable mScrollDownRunnable = new Runnable() { 728 @Override 729 public void run() { 730 scrollDown(); 731 } 732 }; 733 734 private int getVelocity() { 735 float duration = (System.currentTimeMillis() - mDragScrollStart)/mDurationToMax; 736 return (int) Math.min(mMaxVelocity, 737 (int) (mEdgeInterpolator.getInterpolation(duration) * mMaxVelocity)); 738 } 739 740 private void scrollUp() { 741 if (mDragMode == DRAG_MODE_SCROLL_UP) { 742 mScrollView.setScrollY(mScrollView.getScrollY() - getVelocity()); 743 invalidate(); 744 post(mScrollUpRunnable); 745 } 746 } 747 748 private void scrollDown() { 749 if (mDragMode == DRAG_MODE_SCROLL_DOWN) { 750 mScrollView.setScrollY(mScrollView.getScrollY() + getVelocity()); 751 invalidate(); 752 post(mScrollDownRunnable); 753 } 754 } 755 756 // This is used to compute the visual center of the dragView. The idea is that 757 // the visual center represents the user's interpretation of where the item is, and hence 758 // is the appropriate point to use when determining drop location. 759 private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset, 760 DragView dragView, float[] recycle) { 761 float res[]; 762 if (recycle == null) { 763 res = new float[2]; 764 } else { 765 res = recycle; 766 } 767 768 // These represent the visual top and left of drag view if a dragRect was provided. 769 // If a dragRect was not provided, then they correspond to the actual view left and 770 // top, as the dragRect is in that case taken to be the entire dragView. 771 // R.dimen.dragViewOffsetY. 772 int left = x - xOffset; 773 int top = y - yOffset; 774 775 // In order to find the visual center, we shift by half the dragRect 776 res[0] = left + dragView.getDragRegion().width() / 2; 777 res[1] = top + dragView.getDragRegion().height() / 2; 778 779 return res; 780 } 781 782 OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() { 783 public void onAlarm(Alarm alarm) { 784 completeDragExit(); 785 } 786 }; 787 788 public void completeDragExit() { 789 mLauncher.closeFolder(); 790 mCurrentDragInfo = null; 791 mCurrentDragView = null; 792 mSuppressOnAdd = false; 793 mRearrangeOnClose = true; 794 } 795 796 public void onDragExit(DragObject d) { 797 // We only close the folder if this is a true drag exit, ie. not because a drop 798 // has occurred above the folder. 799 if (!d.dragComplete) { 800 mOnExitAlarm.setOnAlarmListener(mOnExitAlarmListener); 801 mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY); 802 } 803 mReorderAlarm.cancelAlarm(); 804 mDragMode = DRAG_MODE_NONE; 805 } 806 807 public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete, 808 boolean success) { 809 if (success) { 810 if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon) { 811 replaceFolderWithFinalItem(); 812 } 813 } else { 814 setupContentForNumItems(getItemCount()); 815 // The drag failed, we need to return the item to the folder 816 mFolderIcon.onDrop(d); 817 } 818 819 if (target != this) { 820 if (mOnExitAlarm.alarmPending()) { 821 mOnExitAlarm.cancelAlarm(); 822 if (!success) { 823 mSuppressFolderDeletion = true; 824 } 825 completeDragExit(); 826 } 827 } 828 mDeleteFolderOnDropCompleted = false; 829 mDragInProgress = false; 830 mItemAddedBackToSelfViaIcon = false; 831 mCurrentDragInfo = null; 832 mCurrentDragView = null; 833 mSuppressOnAdd = false; 834 835 // Reordering may have occured, and we need to save the new item locations. We do this once 836 // at the end to prevent unnecessary database operations. 837 updateItemLocationsInDatabaseBatch(); 838 } 839 840 @Override 841 public boolean supportsFlingToDelete() { 842 return true; 843 } 844 845 public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { 846 // Do nothing 847 } 848 849 @Override 850 public void onFlingToDeleteCompleted() { 851 // Do nothing 852 } 853 854 private void updateItemLocationsInDatabase() { 855 ArrayList<View> list = getItemsInReadingOrder(); 856 for (int i = 0; i < list.size(); i++) { 857 View v = list.get(i); 858 ItemInfo info = (ItemInfo) v.getTag(); 859 LauncherModel.moveItemInDatabase(mLauncher, info, mInfo.id, 0, 860 info.cellX, info.cellY); 861 } 862 } 863 864 private void updateItemLocationsInDatabaseBatch() { 865 ArrayList<View> list = getItemsInReadingOrder(); 866 ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); 867 for (int i = 0; i < list.size(); i++) { 868 View v = list.get(i); 869 ItemInfo info = (ItemInfo) v.getTag(); 870 items.add(info); 871 } 872 873 LauncherModel.moveItemsInDatabase(mLauncher, items, mInfo.id, 0); 874 } 875 876 public void addItemLocationsInDatabase() { 877 ArrayList<View> list = getItemsInReadingOrder(); 878 for (int i = 0; i < list.size(); i++) { 879 View v = list.get(i); 880 ItemInfo info = (ItemInfo) v.getTag(); 881 LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0, 882 info.cellX, info.cellY, false); 883 } 884 } 885 886 public void notifyDrop() { 887 if (mDragInProgress) { 888 mItemAddedBackToSelfViaIcon = true; 889 } 890 } 891 892 public boolean isDropEnabled() { 893 return true; 894 } 895 896 public DropTarget getDropTargetDelegate(DragObject d) { 897 return null; 898 } 899 900 private void setupContentDimensions(int count) { 901 ArrayList<View> list = getItemsInReadingOrder(); 902 903 int countX = mContent.getCountX(); 904 int countY = mContent.getCountY(); 905 boolean done = false; 906 907 while (!done) { 908 int oldCountX = countX; 909 int oldCountY = countY; 910 if (countX * countY < count) { 911 // Current grid is too small, expand it 912 if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) { 913 countX++; 914 } else if (countY < mMaxCountY) { 915 countY++; 916 } 917 if (countY == 0) countY++; 918 } else if ((countY - 1) * countX >= count && countY >= countX) { 919 countY = Math.max(0, countY - 1); 920 } else if ((countX - 1) * countY >= count) { 921 countX = Math.max(0, countX - 1); 922 } 923 done = countX == oldCountX && countY == oldCountY; 924 } 925 mContent.setGridSize(countX, countY); 926 arrangeChildren(list); 927 } 928 929 public boolean isFull() { 930 return getItemCount() >= mMaxNumItems; 931 } 932 933 private void centerAboutIcon() { 934 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 935 936 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); 937 int height = getFolderHeight(); 938 DragLayer parent = (DragLayer) mLauncher.findViewById(R.id.drag_layer); 939 940 float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect); 941 942 int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2); 943 int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2); 944 int centeredLeft = centerX - width / 2; 945 int centeredTop = centerY - height / 2; 946 947 int currentPage = mLauncher.getWorkspace().getCurrentPage(); 948 // In case the workspace is scrolling, we need to use the final scroll to compute 949 // the folders bounds. 950 mLauncher.getWorkspace().setFinalScrollForPageChange(currentPage); 951 // We first fetch the currently visible CellLayoutChildren 952 CellLayout currentLayout = (CellLayout) mLauncher.getWorkspace().getChildAt(currentPage); 953 ShortcutAndWidgetContainer boundingLayout = currentLayout.getShortcutsAndWidgets(); 954 Rect bounds = new Rect(); 955 parent.getDescendantRectRelativeToSelf(boundingLayout, bounds); 956 // We reset the workspaces scroll 957 mLauncher.getWorkspace().resetFinalScrollForPageChange(currentPage); 958 959 // We need to bound the folder to the currently visible CellLayoutChildren 960 int left = Math.min(Math.max(bounds.left, centeredLeft), 961 bounds.left + bounds.width() - width); 962 int top = Math.min(Math.max(bounds.top, centeredTop), 963 bounds.top + bounds.height() - height); 964 // If the folder doesn't fit within the bounds, center it about the desired bounds 965 if (width >= bounds.width()) { 966 left = bounds.left + (bounds.width() - width) / 2; 967 } 968 if (height >= bounds.height()) { 969 top = bounds.top + (bounds.height() - height) / 2; 970 } 971 972 int folderPivotX = width / 2 + (centeredLeft - left); 973 int folderPivotY = height / 2 + (centeredTop - top); 974 setPivotX(folderPivotX); 975 setPivotY(folderPivotY); 976 mFolderIconPivotX = (int) (mFolderIcon.getMeasuredWidth() * 977 (1.0f * folderPivotX / width)); 978 mFolderIconPivotY = (int) (mFolderIcon.getMeasuredHeight() * 979 (1.0f * folderPivotY / height)); 980 981 lp.width = width; 982 lp.height = height; 983 lp.x = left; 984 lp.y = top; 985 } 986 987 float getPivotXForIconAnimation() { 988 return mFolderIconPivotX; 989 } 990 float getPivotYForIconAnimation() { 991 return mFolderIconPivotY; 992 } 993 994 private void setupContentForNumItems(int count) { 995 setupContentDimensions(count); 996 997 DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams(); 998 if (lp == null) { 999 lp = new DragLayer.LayoutParams(0, 0); 1000 lp.customPosition = true; 1001 setLayoutParams(lp); 1002 } 1003 centerAboutIcon(); 1004 } 1005 1006 private int getFolderHeight() { 1007 int contentAreaHeight = mContent.getDesiredHeight(); 1008 if (contentAreaHeight >= mMaxContentAreaHeight) { 1009 // Subtract a bit so the user can see that it's scrollable. 1010 contentAreaHeight = mMaxContentAreaHeight; 1011 } 1012 int height = getPaddingTop() + getPaddingBottom() + contentAreaHeight 1013 + mFolderNameHeight; 1014 return height; 1015 } 1016 1017 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1018 1019 int contentAreaHeight = mContent.getDesiredHeight(); 1020 if (contentAreaHeight >= mMaxContentAreaHeight) { 1021 // Subtract a bit so the user can see that it's scrollable. 1022 contentAreaHeight = mMaxContentAreaHeight; 1023 } 1024 1025 int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth(); 1026 int height = getFolderHeight(); 1027 int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(mContent.getDesiredWidth(), 1028 MeasureSpec.EXACTLY); 1029 int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentAreaHeight, 1030 MeasureSpec.EXACTLY); 1031 1032 mContent.setFixedSize(mContent.getDesiredWidth(), mContent.getDesiredHeight()); 1033 mScrollView.measure(contentAreaWidthSpec, contentAreaHeightSpec); 1034 mFolderName.measure(contentAreaWidthSpec, 1035 MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY)); 1036 setMeasuredDimension(width, height); 1037 } 1038 1039 private void arrangeChildren(ArrayList<View> list) { 1040 int[] vacant = new int[2]; 1041 if (list == null) { 1042 list = getItemsInReadingOrder(); 1043 } 1044 mContent.removeAllViews(); 1045 1046 for (int i = 0; i < list.size(); i++) { 1047 View v = list.get(i); 1048 mContent.getVacantCell(vacant, 1, 1); 1049 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams(); 1050 lp.cellX = vacant[0]; 1051 lp.cellY = vacant[1]; 1052 ItemInfo info = (ItemInfo) v.getTag(); 1053 if (info.cellX != vacant[0] || info.cellY != vacant[1]) { 1054 info.cellX = vacant[0]; 1055 info.cellY = vacant[1]; 1056 LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0, 1057 info.cellX, info.cellY); 1058 } 1059 boolean insert = false; 1060 mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true); 1061 } 1062 mItemsInvalidated = true; 1063 } 1064 1065 public int getItemCount() { 1066 return mContent.getShortcutsAndWidgets().getChildCount(); 1067 } 1068 1069 public View getItemAt(int index) { 1070 return mContent.getShortcutsAndWidgets().getChildAt(index); 1071 } 1072 1073 private void onCloseComplete() { 1074 DragLayer parent = (DragLayer) getParent(); 1075 if (parent != null) { 1076 parent.removeView(this); 1077 } 1078 mDragController.removeDropTarget((DropTarget) this); 1079 clearFocus(); 1080 mFolderIcon.requestFocus(); 1081 1082 if (mRearrangeOnClose) { 1083 setupContentForNumItems(getItemCount()); 1084 mRearrangeOnClose = false; 1085 } 1086 if (getItemCount() <= 1) { 1087 if (!mDragInProgress && !mSuppressFolderDeletion) { 1088 replaceFolderWithFinalItem(); 1089 } else if (mDragInProgress) { 1090 mDeleteFolderOnDropCompleted = true; 1091 } 1092 } 1093 mSuppressFolderDeletion = false; 1094 } 1095 1096 private void replaceFolderWithFinalItem() { 1097 // Add the last remaining child to the workspace in place of the folder 1098 Runnable onCompleteRunnable = new Runnable() { 1099 @Override 1100 public void run() { 1101 CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screenId); 1102 1103 View child = null; 1104 // Move the item from the folder to the workspace, in the position of the folder 1105 if (getItemCount() == 1) { 1106 ShortcutInfo finalItem = mInfo.contents.get(0); 1107 child = mLauncher.createShortcut(R.layout.application, cellLayout, 1108 finalItem); 1109 LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container, 1110 mInfo.screenId, mInfo.cellX, mInfo.cellY); 1111 } 1112 if (getItemCount() <= 1) { 1113 // Remove the folder 1114 LauncherModel.deleteItemFromDatabase(mLauncher, mInfo); 1115 cellLayout.removeView(mFolderIcon); 1116 if (mFolderIcon instanceof DropTarget) { 1117 mDragController.removeDropTarget((DropTarget) mFolderIcon); 1118 } 1119 mLauncher.removeFolder(mInfo); 1120 } 1121 // We add the child after removing the folder to prevent both from existing at 1122 // the same time in the CellLayout. 1123 if (child != null) { 1124 mLauncher.getWorkspace().addInScreen(child, mInfo.container, mInfo.screenId, 1125 mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY); 1126 } 1127 } 1128 }; 1129 View finalChild = getItemAt(0); 1130 if (finalChild != null) { 1131 mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable); 1132 } 1133 mDestroyed = true; 1134 } 1135 1136 boolean isDestroyed() { 1137 return mDestroyed; 1138 } 1139 1140 // This method keeps track of the last item in the folder for the purposes 1141 // of keyboard focus 1142 private void updateTextViewFocus() { 1143 View lastChild = getItemAt(getItemCount() - 1); 1144 getItemAt(getItemCount() - 1); 1145 if (lastChild != null) { 1146 mFolderName.setNextFocusDownId(lastChild.getId()); 1147 mFolderName.setNextFocusRightId(lastChild.getId()); 1148 mFolderName.setNextFocusLeftId(lastChild.getId()); 1149 mFolderName.setNextFocusUpId(lastChild.getId()); 1150 } 1151 } 1152 1153 public void onDrop(DragObject d) { 1154 ShortcutInfo item; 1155 if (d.dragInfo instanceof ApplicationInfo) { 1156 // Came from all apps -- make a copy 1157 item = ((ApplicationInfo) d.dragInfo).makeShortcut(); 1158 item.spanX = 1; 1159 item.spanY = 1; 1160 } else { 1161 item = (ShortcutInfo) d.dragInfo; 1162 } 1163 // Dragged from self onto self, currently this is the only path possible, however 1164 // we keep this as a distinct code path. 1165 if (item == mCurrentDragInfo) { 1166 ShortcutInfo si = (ShortcutInfo) mCurrentDragView.getTag(); 1167 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mCurrentDragView.getLayoutParams(); 1168 si.cellX = lp.cellX = mEmptyCell[0]; 1169 si.cellX = lp.cellY = mEmptyCell[1]; 1170 mContent.addViewToCellLayout(mCurrentDragView, -1, (int)item.id, lp, true); 1171 if (d.dragView.hasDrawn()) { 1172 mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, mCurrentDragView); 1173 } else { 1174 d.deferDragViewCleanupPostAnimation = false; 1175 mCurrentDragView.setVisibility(VISIBLE); 1176 } 1177 mItemsInvalidated = true; 1178 setupContentDimensions(getItemCount()); 1179 mSuppressOnAdd = true; 1180 } 1181 mInfo.add(item); 1182 } 1183 1184 // This is used so the item doesn't immediately appear in the folder when added. In one case 1185 // we need to create the illusion that the item isn't added back to the folder yet, to 1186 // to correspond to the animation of the icon back into the folder. This is 1187 public void hideItem(ShortcutInfo info) { 1188 View v = getViewForInfo(info); 1189 v.setVisibility(INVISIBLE); 1190 } 1191 public void showItem(ShortcutInfo info) { 1192 View v = getViewForInfo(info); 1193 v.setVisibility(VISIBLE); 1194 } 1195 1196 public void onAdd(ShortcutInfo item) { 1197 mItemsInvalidated = true; 1198 // If the item was dropped onto this open folder, we have done the work associated 1199 // with adding the item to the folder, as indicated by mSuppressOnAdd being set 1200 if (mSuppressOnAdd) return; 1201 if (!findAndSetEmptyCells(item)) { 1202 // The current layout is full, can we expand it? 1203 setupContentForNumItems(getItemCount() + 1); 1204 findAndSetEmptyCells(item); 1205 } 1206 createAndAddShortcut(item); 1207 LauncherModel.addOrMoveItemInDatabase( 1208 mLauncher, item, mInfo.id, 0, item.cellX, item.cellY); 1209 } 1210 1211 public void onRemove(ShortcutInfo item) { 1212 mItemsInvalidated = true; 1213 // If this item is being dragged from this open folder, we have already handled 1214 // the work associated with removing the item, so we don't have to do anything here. 1215 if (item == mCurrentDragInfo) return; 1216 View v = getViewForInfo(item); 1217 mContent.removeView(v); 1218 if (mState == STATE_ANIMATING) { 1219 mRearrangeOnClose = true; 1220 } else { 1221 setupContentForNumItems(getItemCount()); 1222 } 1223 if (getItemCount() <= 1) { 1224 replaceFolderWithFinalItem(); 1225 } 1226 } 1227 1228 private View getViewForInfo(ShortcutInfo item) { 1229 for (int j = 0; j < mContent.getCountY(); j++) { 1230 for (int i = 0; i < mContent.getCountX(); i++) { 1231 View v = mContent.getChildAt(i, j); 1232 if (v.getTag() == item) { 1233 return v; 1234 } 1235 } 1236 } 1237 return null; 1238 } 1239 1240 public void onItemsChanged() { 1241 updateTextViewFocus(); 1242 } 1243 1244 public void onTitleChanged(CharSequence title) { 1245 } 1246 1247 public ArrayList<View> getItemsInReadingOrder() { 1248 if (mItemsInvalidated) { 1249 mItemsInReadingOrder.clear(); 1250 for (int j = 0; j < mContent.getCountY(); j++) { 1251 for (int i = 0; i < mContent.getCountX(); i++) { 1252 View v = mContent.getChildAt(i, j); 1253 if (v != null) { 1254 mItemsInReadingOrder.add(v); 1255 } 1256 } 1257 } 1258 mItemsInvalidated = false; 1259 } 1260 return mItemsInReadingOrder; 1261 } 1262 1263 public void getLocationInDragLayer(int[] loc) { 1264 mLauncher.getDragLayer().getLocationInDragLayer(this, loc); 1265 } 1266 1267 public void onFocusChange(View v, boolean hasFocus) { 1268 if (v == mFolderName && hasFocus) { 1269 startEditingFolderName(); 1270 } 1271 } 1272} 1273