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 */ 16 package com.android.dialer.list; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorSet; 20 import android.animation.ObjectAnimator; 21 import android.app.Activity; 22 import android.app.Fragment; 23 import android.app.LoaderManager; 24 import android.content.CursorLoader; 25 import android.content.Loader; 26 import android.content.res.Resources; 27 import android.database.Cursor; 28 import android.graphics.Rect; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.util.Log; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.ViewTreeObserver; 36 import android.view.animation.AnimationUtils; 37 import android.view.animation.LayoutAnimationController; 38 import android.widget.AbsListView; 39 import android.widget.AdapterView; 40 import android.widget.AdapterView.OnItemClickListener; 41 import android.widget.ImageView; 42 import android.widget.ListView; 43 import android.widget.RelativeLayout; 44 import android.widget.RelativeLayout.LayoutParams; 45 46 import com.android.contacts.common.ContactPhotoManager; 47 import com.android.contacts.common.ContactTileLoaderFactory; 48 import com.android.contacts.common.list.ContactTileView; 49 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 50 import com.android.dialer.R; 51 import com.android.dialer.util.DialerUtils; 52 53 import java.util.ArrayList; 54 import java.util.HashMap; 55 56 /** 57 * This fragment displays the user's favorite/frequent contacts in a grid. 58 */ 59 public class SpeedDialFragment extends Fragment 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 emptyViewVisibility = visible ? View.VISIBLE : View.GONE; 239 final int listViewVisibility = visible ? View.GONE : View.VISIBLE; 240 241 if (previousVisibility != emptyViewVisibility) { 242 final RelativeLayout.LayoutParams params = (LayoutParams) mContactTileFrame 243 .getLayoutParams(); 244 params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; 245 mContactTileFrame.setLayoutParams(params); 246 mEmptyView.setVisibility(emptyViewVisibility); 247 mListView.setVisibility(listViewVisibility); 248 } 249 } 250 251 @Override 252 public void onStart() { 253 super.onStart(); 254 255 final Activity activity = getActivity(); 256 257 try { 258 mActivityScrollListener = (OnListFragmentScrolledListener) activity; 259 } catch (ClassCastException e) { 260 throw new ClassCastException(activity.toString() 261 + " must implement OnListFragmentScrolledListener"); 262 } 263 264 try { 265 OnDragDropListener listener = (OnDragDropListener) activity; 266 mListView.getDragDropController().addOnDragDropListener(listener); 267 ((HostInterface) activity).setDragDropController(mListView.getDragDropController()); 268 } catch (ClassCastException e) { 269 throw new ClassCastException(activity.toString() 270 + " must implement OnDragDropListener and HostInterface"); 271 } 272 273 try { 274 mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity; 275 } catch (ClassCastException e) { 276 throw new ClassCastException(activity.toString() 277 + " must implement PhoneFavoritesFragment.listener"); 278 } 279 280 // Use initLoader() instead of restartLoader() to refraining unnecessary reload. 281 // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will 282 // be called, on which we'll check if "all" contacts should be reloaded again or not. 283 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); 284 } 285 286 /** 287 * {@inheritDoc} 288 * 289 * This is only effective for elements provided by {@link #mContactTileAdapter}. 290 * {@link #mContactTileAdapter} has its own logic for click events. 291 */ 292 @Override 293 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 294 final int contactTileAdapterCount = mContactTileAdapter.getCount(); 295 if (position <= contactTileAdapterCount) { 296 Log.e(TAG, "onItemClick() event for unexpected position. " 297 + "The position " + position + " is before \"all\" section. Ignored."); 298 } 299 } 300 301 /** 302 * Cache the current view offsets into memory. Once a relayout of views in the ListView 303 * has happened due to a dataset change, the cached offsets are used to create animations 304 * that slide views from their previous positions to their new ones, to give the appearance 305 * that the views are sliding into their new positions. 306 */ 307 private void saveOffsets(int removedItemHeight) { 308 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 309 if (DEBUG) { 310 Log.d(TAG, "Child count : " + mListView.getChildCount()); 311 } 312 for (int i = 0; i < mListView.getChildCount(); i++) { 313 final View child = mListView.getChildAt(i); 314 final int position = firstVisiblePosition + i; 315 // Since we are getting the position from mListView and then querying 316 // mContactTileAdapter, its very possible that things are out of sync 317 // and we might index out of bounds. Let's make sure that this doesn't happen. 318 if (!mContactTileAdapter.isIndexInBound(position)) { 319 continue; 320 } 321 final long itemId = mContactTileAdapter.getItemId(position); 322 if (DEBUG) { 323 Log.d(TAG, "Saving itemId: " + itemId + " for listview child " + i + " Top: " 324 + child.getTop()); 325 } 326 mItemIdTopMap.put(itemId, child.getTop()); 327 mItemIdLeftMap.put(itemId, child.getLeft()); 328 } 329 mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight); 330 } 331 332 /* 333 * Performs animations for the gridView 334 */ 335 private void animateGridView(final long... idsInPlace) { 336 if (mItemIdTopMap.isEmpty()) { 337 // Don't do animations if the database is being queried for the first time and 338 // the previous item offsets have not been cached, or the user hasn't done anything 339 // (dragging, swiping etc) that requires an animation. 340 return; 341 } 342 343 final ViewTreeObserver observer = mListView.getViewTreeObserver(); 344 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 345 @SuppressWarnings("unchecked") 346 @Override 347 public boolean onPreDraw() { 348 observer.removeOnPreDrawListener(this); 349 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 350 final AnimatorSet animSet = new AnimatorSet(); 351 final ArrayList<Animator> animators = new ArrayList<Animator>(); 352 for (int i = 0; i < mListView.getChildCount(); i++) { 353 final View child = mListView.getChildAt(i); 354 int position = firstVisiblePosition + i; 355 356 // Since we are getting the position from mListView and then querying 357 // mContactTileAdapter, its very possible that things are out of sync 358 // and we might index out of bounds. Let's make sure that this doesn't happen. 359 if (!mContactTileAdapter.isIndexInBound(position)) { 360 continue; 361 } 362 363 final long itemId = mContactTileAdapter.getItemId(position); 364 365 if (containsId(idsInPlace, itemId)) { 366 animators.add(ObjectAnimator.ofFloat( 367 child, "alpha", 0.0f, 1.0f)); 368 break; 369 } else { 370 Integer startTop = mItemIdTopMap.get(itemId); 371 Integer startLeft = mItemIdLeftMap.get(itemId); 372 final int top = child.getTop(); 373 final int left = child.getLeft(); 374 int deltaX = 0; 375 int deltaY = 0; 376 377 if (startLeft != null) { 378 if (startLeft != left) { 379 deltaX = startLeft - left; 380 animators.add(ObjectAnimator.ofFloat( 381 child, "translationX", deltaX, 0.0f)); 382 } 383 } 384 385 if (startTop != null) { 386 if (startTop != top) { 387 deltaY = startTop - top; 388 animators.add(ObjectAnimator.ofFloat( 389 child, "translationY", deltaY, 0.0f)); 390 } 391 } 392 393 if (DEBUG) { 394 Log.d(TAG, "Found itemId: " + itemId + " for listview child " + i + 395 " Top: " + top + 396 " Delta: " + deltaY); 397 } 398 } 399 } 400 401 if (animators.size() > 0) { 402 animSet.setDuration(mAnimationDuration).playTogether(animators); 403 animSet.start(); 404 } 405 406 mItemIdTopMap.clear(); 407 mItemIdLeftMap.clear(); 408 return true; 409 } 410 }); 411 } 412 413 private boolean containsId(long[] ids, long target) { 414 // Linear search on array is fine because this is typically only 0-1 elements long 415 for (int i = 0; i < ids.length; i++) { 416 if (ids[i] == target) { 417 return true; 418 } 419 } 420 return false; 421 } 422 423 @Override 424 public void onDataSetChangedForAnimation(long... idsInPlace) { 425 animateGridView(idsInPlace); 426 } 427 428 @Override 429 public void cacheOffsetsForDatasetChange() { 430 saveOffsets(0); 431 } 432 433 public AbsListView getListView() { 434 return mListView; 435 } 436 } 437