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.LoaderManager; 23 import android.content.CursorLoader; 24 import android.content.Loader; 25 import android.content.res.Resources; 26 import android.database.Cursor; 27 import android.graphics.Rect; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.util.Log; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.view.ViewTreeObserver; 35 import android.view.animation.AnimationUtils; 36 import android.view.animation.LayoutAnimationController; 37 import android.widget.AbsListView; 38 import android.widget.AdapterView; 39 import android.widget.AdapterView.OnItemClickListener; 40 import android.widget.ImageView; 41 import android.widget.ListView; 42 import android.widget.RelativeLayout; 43 import android.widget.RelativeLayout.LayoutParams; 44 45 import com.android.contacts.common.ContactPhotoManager; 46 import com.android.contacts.common.ContactTileLoaderFactory; 47 import com.android.contacts.common.list.ContactTileView; 48 import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; 49 import com.android.dialer.R; 50 import com.android.dialer.util.DialerUtils; 51 import com.android.dialerbind.analytics.AnalyticsFragment; 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 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 TileInteractionTeaserView mTileInteractionTeaserView; 154 155 private final HashMap<Long, Integer> mItemIdTopMap = new HashMap<Long, Integer>(); 156 private final HashMap<Long, Integer> mItemIdLeftMap = new HashMap<Long, Integer>(); 157 158 /** 159 * Layout used when there are no favorites. 160 */ 161 private View mEmptyView; 162 163 private final ContactTileView.Listener mContactTileAdapterListener = 164 new ContactTileAdapterListener(); 165 private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener = 166 new ContactTileLoaderListener(); 167 private final ScrollListener mScrollListener = new ScrollListener(); 168 169 @Override 170 public void onAttach(Activity activity) { 171 if (DEBUG) Log.d(TAG, "onAttach()"); 172 super.onAttach(activity); 173 174 // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter. 175 // We don't construct the resultant adapter at this moment since it requires LayoutInflater 176 // that will be available on onCreateView(). 177 mContactTileAdapter = new PhoneFavoritesTileAdapter(activity, mContactTileAdapterListener, 178 this); 179 mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity)); 180 } 181 182 @Override 183 public void onCreate(Bundle savedState) { 184 if (DEBUG) Log.d(TAG, "onCreate()"); 185 super.onCreate(savedState); 186 187 mAnimationDuration = getResources().getInteger(R.integer.fade_duration); 188 } 189 190 @Override 191 public void onResume() { 192 super.onResume(); 193 194 getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); 195 } 196 197 @Override 198 public View onCreateView(LayoutInflater inflater, ViewGroup container, 199 Bundle savedInstanceState) { 200 mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false); 201 202 mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list); 203 mListView.setOnItemClickListener(this); 204 mListView.setVerticalScrollBarEnabled(false); 205 mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); 206 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); 207 mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter); 208 209 final ImageView dragShadowOverlay = 210 (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay); 211 mListView.setDragShadowOverlay(dragShadowOverlay); 212 213 final Resources resources = getResources(); 214 mEmptyView = mParentView.findViewById(R.id.empty_list_view); 215 DialerUtils.configureEmptyListView( 216 mEmptyView, R.drawable.empty_speed_dial, R.string.speed_dial_empty, getResources()); 217 218 mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame); 219 220 mTileInteractionTeaserView = (TileInteractionTeaserView) inflater.inflate( 221 R.layout.tile_interactions_teaser_view, mListView, false); 222 223 final LayoutAnimationController controller = new LayoutAnimationController( 224 AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); 225 controller.setDelay(0); 226 mListView.setLayoutAnimation(controller); 227 mListView.setAdapter(mContactTileAdapter); 228 229 mListView.setOnScrollListener(mScrollListener); 230 mListView.setFastScrollEnabled(false); 231 mListView.setFastScrollAlwaysVisible(false); 232 233 return mParentView; 234 } 235 236 public boolean hasFrequents() { 237 if (mContactTileAdapter == null) return false; 238 return mContactTileAdapter.getNumFrequents() > 0; 239 } 240 241 /* package */ void setEmptyViewVisibility(final boolean visible) { 242 final int previousVisibility = mEmptyView.getVisibility(); 243 final int newVisibility = visible ? View.VISIBLE : View.GONE; 244 245 if (previousVisibility != newVisibility) { 246 final RelativeLayout.LayoutParams params = (LayoutParams) mContactTileFrame 247 .getLayoutParams(); 248 params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; 249 mContactTileFrame.setLayoutParams(params); 250 mEmptyView.setVisibility(newVisibility); 251 } 252 } 253 254 @Override 255 public void onStart() { 256 super.onStart(); 257 258 final Activity activity = getActivity(); 259 260 try { 261 mActivityScrollListener = (OnListFragmentScrolledListener) activity; 262 } catch (ClassCastException e) { 263 throw new ClassCastException(activity.toString() 264 + " must implement OnListFragmentScrolledListener"); 265 } 266 267 try { 268 OnDragDropListener listener = (OnDragDropListener) activity; 269 mListView.getDragDropController().addOnDragDropListener(listener); 270 ((HostInterface) activity).setDragDropController(mListView.getDragDropController()); 271 } catch (ClassCastException e) { 272 throw new ClassCastException(activity.toString() 273 + " must implement OnDragDropListener and HostInterface"); 274 } 275 276 try { 277 mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity; 278 } catch (ClassCastException e) { 279 throw new ClassCastException(activity.toString() 280 + " must implement PhoneFavoritesFragment.listener"); 281 } 282 283 // Use initLoader() instead of restartLoader() to refraining unnecessary reload. 284 // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will 285 // be called, on which we'll check if "all" contacts should be reloaded again or not. 286 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); 287 } 288 289 /** 290 * {@inheritDoc} 291 * 292 * This is only effective for elements provided by {@link #mContactTileAdapter}. 293 * {@link #mContactTileAdapter} has its own logic for click events. 294 */ 295 @Override 296 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 297 final int contactTileAdapterCount = mContactTileAdapter.getCount(); 298 if (position <= contactTileAdapterCount) { 299 Log.e(TAG, "onItemClick() event for unexpected position. " 300 + "The position " + position + " is before \"all\" section. Ignored."); 301 } 302 } 303 304 /** 305 * Cache the current view offsets into memory. Once a relayout of views in the ListView 306 * has happened due to a dataset change, the cached offsets are used to create animations 307 * that slide views from their previous positions to their new ones, to give the appearance 308 * that the views are sliding into their new positions. 309 */ 310 private void saveOffsets(int removedItemHeight) { 311 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 312 if (DEBUG) { 313 Log.d(TAG, "Child count : " + mListView.getChildCount()); 314 } 315 for (int i = 0; i < mListView.getChildCount(); i++) { 316 final View child = mListView.getChildAt(i); 317 final int position = firstVisiblePosition + i; 318 final long itemId = mContactTileAdapter.getItemId(position); 319 if (DEBUG) { 320 Log.d(TAG, "Saving itemId: " + itemId + " for listview child " + i + " Top: " 321 + child.getTop()); 322 } 323 mItemIdTopMap.put(itemId, child.getTop()); 324 mItemIdLeftMap.put(itemId, child.getLeft()); 325 } 326 327 mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight); 328 } 329 330 /* 331 * Performs animations for the gridView 332 */ 333 private void animateGridView(final long... idsInPlace) { 334 if (mItemIdTopMap.isEmpty()) { 335 // Don't do animations if the database is being queried for the first time and 336 // the previous item offsets have not been cached, or the user hasn't done anything 337 // (dragging, swiping etc) that requires an animation. 338 return; 339 } 340 341 final ViewTreeObserver observer = mListView.getViewTreeObserver(); 342 observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 343 @SuppressWarnings("unchecked") 344 @Override 345 public boolean onPreDraw() { 346 observer.removeOnPreDrawListener(this); 347 final int firstVisiblePosition = mListView.getFirstVisiblePosition(); 348 final AnimatorSet animSet = new AnimatorSet(); 349 final ArrayList<Animator> animators = new ArrayList<Animator>(); 350 for (int i = 0; i < mListView.getChildCount(); i++) { 351 final View child = mListView.getChildAt(i); 352 int position = firstVisiblePosition + i; 353 354 final long itemId = mContactTileAdapter.getItemId(position); 355 356 if (containsId(idsInPlace, itemId)) { 357 animators.add(ObjectAnimator.ofFloat( 358 child, "alpha", 0.0f, 1.0f)); 359 break; 360 } else { 361 Integer startTop = mItemIdTopMap.get(itemId); 362 Integer startLeft = mItemIdLeftMap.get(itemId); 363 final int top = child.getTop(); 364 final int left = child.getLeft(); 365 int deltaX = 0; 366 int deltaY = 0; 367 368 if (startLeft != null) { 369 if (startLeft != left) { 370 deltaX = startLeft - left; 371 animators.add(ObjectAnimator.ofFloat( 372 child, "translationX", deltaX, 0.0f)); 373 } 374 } 375 376 if (startTop != null) { 377 if (startTop != top) { 378 deltaY = startTop - top; 379 animators.add(ObjectAnimator.ofFloat( 380 child, "translationY", deltaY, 0.0f)); 381 } 382 } 383 384 if (DEBUG) { 385 Log.d(TAG, "Found itemId: " + itemId + " for listview child " + i + 386 " Top: " + top + 387 " Delta: " + deltaY); 388 } 389 } 390 } 391 392 if (animators.size() > 0) { 393 animSet.setDuration(mAnimationDuration).playTogether(animators); 394 animSet.start(); 395 } 396 397 mItemIdTopMap.clear(); 398 mItemIdLeftMap.clear(); 399 return true; 400 } 401 }); 402 } 403 404 private boolean containsId(long[] ids, long target) { 405 // Linear search on array is fine because this is typically only 0-1 elements long 406 for (int i = 0; i < ids.length; i++) { 407 if (ids[i] == target) { 408 return true; 409 } 410 } 411 return false; 412 } 413 414 @Override 415 public void onDataSetChangedForAnimation(long... idsInPlace) { 416 animateGridView(idsInPlace); 417 } 418 419 @Override 420 public void cacheOffsetsForDatasetChange() { 421 saveOffsets(0); 422 } 423 424 public AbsListView getListView() { 425 return mListView; 426 } 427 } 428