SpeedDialFragment.java revision 967cdb018142c388e493213f0b97f40ac669578e
1/* 2 * Copyright (C) 2013 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 */ 16package com.android.dialer.list; 17 18import android.animation.Animator; 19import android.animation.AnimatorSet; 20import android.animation.ObjectAnimator; 21import android.app.Activity; 22import android.app.LoaderManager; 23import android.content.CursorLoader; 24import android.content.Loader; 25import android.content.res.Resources; 26import android.database.Cursor; 27import android.graphics.Rect; 28import android.net.Uri; 29import android.os.Bundle; 30import android.util.Log; 31import android.view.LayoutInflater; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.ViewTreeObserver; 35import android.view.animation.AnimationUtils; 36import android.view.animation.LayoutAnimationController; 37import android.widget.AbsListView; 38import android.widget.AdapterView; 39import android.widget.AdapterView.OnItemClickListener; 40import android.widget.ImageView; 41import android.widget.ListView; 42import android.widget.RelativeLayout; 43import android.widget.RelativeLayout.LayoutParams; 44 45import com.android.contacts.common.ContactPhotoManager; 46import com.android.contacts.common.ContactTileLoaderFactory; 47import com.android.contacts.common.list.ContactTileView; 48import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 49import com.android.dialer.R; 50import com.android.dialer.util.DialerUtils; 51import com.android.dialerbind.analytics.AnalyticsFragment; 52 53import java.util.ArrayList; 54import java.util.HashMap; 55 56/** 57 * This fragment displays the user's favorite/frequent contacts in a grid. 58 */ 59public class SpeedDialFragment extends AnalyticsFragment implements OnItemClickListener, 60 PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener { 61 62 /** 63 * By default, the animation code assumes that all items in a list view are of the same height 64 * when animating new list items into view (e.g. from the bottom of the screen into view). 65 * This can cause incorrect translation offsets when a item that is larger or smaller than 66 * other list item is removed from the list. This key is used to provide the actual height 67 * of the removed object so that the actual translation appears correct to the user. 68 */ 69 private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE; 70 71 private static final String TAG = SpeedDialFragment.class.getSimpleName(); 72 private static final boolean DEBUG = false; 73 74 private int mAnimationDuration; 75 76 /** 77 * Used with LoaderManager. 78 */ 79 private static int LOADER_ID_CONTACT_TILE = 1; 80 81 public interface HostInterface { 82 public void setDragDropController(DragDropController controller); 83 } 84 85 private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> { 86 @Override 87 public CursorLoader onCreateLoader(int id, Bundle args) { 88 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader."); 89 return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity()); 90 } 91 92 @Override 93 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 94 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished"); 95 mContactTileAdapter.setContactCursor(data); 96 setEmptyViewVisibility(mContactTileAdapter.getCount() == 0); 97 } 98 99 @Override 100 public void onLoaderReset(Loader<Cursor> loader) { 101 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. "); 102 } 103 } 104 105 private class ContactTileAdapterListener implements ContactTileView.Listener { 106 @Override 107 public void onContactSelected(Uri contactUri, Rect targetRect) { 108 if (mPhoneNumberPickerActionListener != null) { 109 mPhoneNumberPickerActionListener.onPickPhoneNumberAction(contactUri); 110 } 111 } 112 113 @Override 114 public void onCallNumberDirectly(String phoneNumber) { 115 if (mPhoneNumberPickerActionListener != null) { 116 mPhoneNumberPickerActionListener.onCallNumberDirectly(phoneNumber); 117 } 118 } 119 120 @Override 121 public int getApproximateTileWidth() { 122 return getView().getWidth(); 123 } 124 } 125 126 private class ScrollListener implements ListView.OnScrollListener { 127 @Override 128 public void onScroll(AbsListView view, 129 int firstVisibleItem, int visibleItemCount, int totalItemCount) { 130 if (mActivityScrollListener != null) { 131 mActivityScrollListener.onListFragmentScroll(firstVisibleItem, visibleItemCount, 132 totalItemCount); 133 } 134 } 135 136 @Override 137 public void onScrollStateChanged(AbsListView view, int scrollState) { 138 mActivityScrollListener.onListFragmentScrollStateChange(scrollState); 139 } 140 } 141 142 private OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener; 143 144 private OnListFragmentScrolledListener mActivityScrollListener; 145 private PhoneFavoritesTileAdapter mContactTileAdapter; 146 147 private View mParentView; 148 149 private PhoneFavoriteListView mListView; 150 151 private View mContactTileFrame; 152 153 private final HashMap<Long, Integer> mItemIdTopMap = new HashMap<Long, Integer>(); 154 private final HashMap<Long, Integer> mItemIdLeftMap = new HashMap<Long, Integer>(); 155 156 /** 157 * Layout used when there are no favorites. 158 */ 159 private View mEmptyView; 160 161 private final ContactTileView.Listener mContactTileAdapterListener = 162 new ContactTileAdapterListener(); 163 private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener = 164 new ContactTileLoaderListener(); 165 private final ScrollListener mScrollListener = new ScrollListener(); 166 167 @Override 168 public void onAttach(Activity activity) { 169 if (DEBUG) Log.d(TAG, "onAttach()"); 170 super.onAttach(activity); 171 172 // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter. 173 // We don't construct the resultant adapter at this moment since it requires LayoutInflater 174 // that will be available on onCreateView(). 175 mContactTileAdapter = new PhoneFavoritesTileAdapter(activity, mContactTileAdapterListener, 176 this); 177 mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity)); 178 } 179 180 @Override 181 public void onCreate(Bundle savedState) { 182 if (DEBUG) Log.d(TAG, "onCreate()"); 183 super.onCreate(savedState); 184 185 mAnimationDuration = getResources().getInteger(R.integer.fade_duration); 186 } 187 188 @Override 189 public void onResume() { 190 super.onResume(); 191 192 getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); 193 } 194 195 @Override 196 public View onCreateView(LayoutInflater inflater, ViewGroup container, 197 Bundle savedInstanceState) { 198 mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false); 199 200 mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list); 201 mListView.setOnItemClickListener(this); 202 mListView.setVerticalScrollBarEnabled(false); 203 mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); 204 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); 205 mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter); 206 207 final ImageView dragShadowOverlay = 208 (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay); 209 mListView.setDragShadowOverlay(dragShadowOverlay); 210 211 final Resources resources = getResources(); 212 mEmptyView = mParentView.findViewById(R.id.empty_list_view); 213 DialerUtils.configureEmptyListView( 214 mEmptyView, R.drawable.empty_speed_dial, R.string.speed_dial_empty, getResources()); 215 216 mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame); 217 218 final LayoutAnimationController controller = new LayoutAnimationController( 219 AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); 220 controller.setDelay(0); 221 mListView.setLayoutAnimation(controller); 222 mListView.setAdapter(mContactTileAdapter); 223 224 mListView.setOnScrollListener(mScrollListener); 225 mListView.setFastScrollEnabled(false); 226 mListView.setFastScrollAlwaysVisible(false); 227 228 return mParentView; 229 } 230 231 public boolean hasFrequents() { 232 if (mContactTileAdapter == null) return false; 233 return mContactTileAdapter.getNumFrequents() > 0; 234 } 235 236 /* package */ void setEmptyViewVisibility(final boolean visible) { 237 final int previousVisibility = mEmptyView.getVisibility(); 238 final int newVisibility = visible ? View.VISIBLE : View.GONE; 239 240 if (previousVisibility != newVisibility) { 241 final RelativeLayout.LayoutParams params = (LayoutParams) mContactTileFrame 242 .getLayoutParams(); 243 params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; 244 mContactTileFrame.setLayoutParams(params); 245 mEmptyView.setVisibility(newVisibility); 246 } 247 } 248 249 @Override 250 public void onStart() { 251 super.onStart(); 252 253 final Activity activity = getActivity(); 254 255 try { 256 mActivityScrollListener = (OnListFragmentScrolledListener) activity; 257 } catch (ClassCastException e) { 258 throw new ClassCastException(activity.toString() 259 + " must implement OnListFragmentScrolledListener"); 260 } 261 262 try { 263 OnDragDropListener listener = (OnDragDropListener) activity; 264 mListView.getDragDropController().addOnDragDropListener(listener); 265 ((HostInterface) activity).setDragDropController(mListView.getDragDropController()); 266 } catch (ClassCastException e) { 267 throw new ClassCastException(activity.toString() 268 + " must implement OnDragDropListener and HostInterface"); 269 } 270 271 try { 272 mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity; 273 } catch (ClassCastException e) { 274 throw new ClassCastException(activity.toString() 275 + " must implement PhoneFavoritesFragment.listener"); 276 } 277 278 // Use initLoader() instead of restartLoader() to refraining unnecessary reload. 279 // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will 280 // be called, on which we'll check if "all" contacts should be reloaded again or not. 281 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); 282 } 283 284 /** 285 * {@inheritDoc} 286 * 287 * This is only effective for elements provided by {@link #mContactTileAdapter}. 288 * {@link #mContactTileAdapter} has its own logic for click events. 289 */ 290 @Override 291 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 292 final int contactTileAdapterCount = mContactTileAdapter.getCount(); 293 if (position <= contactTileAdapterCount) { 294 Log.e(TAG, "onItemClick() event for unexpected position. " 295 + "The position " + position + " is before \"all\" section. Ignored."); 296 } 297 } 298 299 /** 300 * Cache the current view offsets into memory. Once a relayout of views in the ListView 301 * has happened due to a dataset change, the cached offsets are used to create animations 302 * that slide views from their previous positions to their new ones, to give the appearance 303 * that the views are sliding into their new positions. 304 */ 305 private void saveOffsets(int removedItemHeight) { 306 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 307 if (DEBUG) { 308 Log.d(TAG, "Child count : " + mListView.getChildCount()); 309 } 310 for (int i = 0; i < mListView.getChildCount(); i++) { 311 final View child = mListView.getChildAt(i); 312 final int position = firstVisiblePosition + i; 313 final long itemId = mContactTileAdapter.getItemId(position); 314 if (DEBUG) { 315 Log.d(TAG, "Saving itemId: " + itemId + " for listview child " + i + " Top: " 316 + child.getTop()); 317 } 318 mItemIdTopMap.put(itemId, child.getTop()); 319 mItemIdLeftMap.put(itemId, child.getLeft()); 320 } 321 322 mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight); 323 } 324 325 /* 326 * Performs animations for the gridView 327 */ 328 private void animateGridView(final long... idsInPlace) { 329 if (mItemIdTopMap.isEmpty()) { 330 // Don't do animations if the database is being queried for the first time and 331 // the previous item offsets have not been cached, or the user hasn't done anything 332 // (dragging, swiping etc) that requires an animation. 333 return; 334 } 335 336 final ViewTreeObserver observer = mListView.getViewTreeObserver(); 337 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 338 @SuppressWarnings("unchecked") 339 @Override 340 public boolean onPreDraw() { 341 observer.removeOnPreDrawListener(this); 342 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 343 final AnimatorSet animSet = new AnimatorSet(); 344 final ArrayList<Animator> animators = new ArrayList<Animator>(); 345 for (int i = 0; i < mListView.getChildCount(); i++) { 346 final View child = mListView.getChildAt(i); 347 int position = firstVisiblePosition + i; 348 349 final long itemId = mContactTileAdapter.getItemId(position); 350 351 if (containsId(idsInPlace, itemId)) { 352 animators.add(ObjectAnimator.ofFloat( 353 child, "alpha", 0.0f, 1.0f)); 354 break; 355 } else { 356 Integer startTop = mItemIdTopMap.get(itemId); 357 Integer startLeft = mItemIdLeftMap.get(itemId); 358 final int top = child.getTop(); 359 final int left = child.getLeft(); 360 int deltaX = 0; 361 int deltaY = 0; 362 363 if (startLeft != null) { 364 if (startLeft != left) { 365 deltaX = startLeft - left; 366 animators.add(ObjectAnimator.ofFloat( 367 child, "translationX", deltaX, 0.0f)); 368 } 369 } 370 371 if (startTop != null) { 372 if (startTop != top) { 373 deltaY = startTop - top; 374 animators.add(ObjectAnimator.ofFloat( 375 child, "translationY", deltaY, 0.0f)); 376 } 377 } 378 379 if (DEBUG) { 380 Log.d(TAG, "Found itemId: " + itemId + " for listview child " + i + 381 " Top: " + top + 382 " Delta: " + deltaY); 383 } 384 } 385 } 386 387 if (animators.size() > 0) { 388 animSet.setDuration(mAnimationDuration).playTogether(animators); 389 animSet.start(); 390 } 391 392 mItemIdTopMap.clear(); 393 mItemIdLeftMap.clear(); 394 return true; 395 } 396 }); 397 } 398 399 private boolean containsId(long[] ids, long target) { 400 // Linear search on array is fine because this is typically only 0-1 elements long 401 for (int i = 0; i < ids.length; i++) { 402 if (ids[i] == target) { 403 return true; 404 } 405 } 406 return false; 407 } 408 409 @Override 410 public void onDataSetChangedForAnimation(long... idsInPlace) { 411 animateGridView(idsInPlace); 412 } 413 414 @Override 415 public void cacheOffsetsForDatasetChange() { 416 saveOffsets(0); 417 } 418 419 public AbsListView getListView() { 420 return mListView; 421 } 422} 423