• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
setDragDropController(DragDropController controller)82         public void setDragDropController(DragDropController controller);
83     }
84 
85     private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
86         @Override
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<Cursor> loader, Cursor data)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
onLoaderReset(Loader<Cursor> loader)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
onContactSelected(Uri contactUri, Rect targetRect)107         public void onContactSelected(Uri contactUri, Rect targetRect) {
108             if (mPhoneNumberPickerActionListener != null) {
109                 mPhoneNumberPickerActionListener.onPickPhoneNumberAction(contactUri);
110             }
111         }
112 
113         @Override
onCallNumberDirectly(String phoneNumber)114         public void onCallNumberDirectly(String phoneNumber) {
115             if (mPhoneNumberPickerActionListener != null) {
116                 mPhoneNumberPickerActionListener.onCallNumberDirectly(phoneNumber);
117             }
118         }
119 
120         @Override
getApproximateTileWidth()121         public int getApproximateTileWidth() {
122             return getView().getWidth();
123         }
124     }
125 
126     private class ScrollListener implements ListView.OnScrollListener {
127         @Override
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)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
onScrollStateChanged(AbsListView view, int scrollState)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
onAttach(Activity activity)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
onCreate(Bundle savedState)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
onResume()189     public void onResume() {
190         super.onResume();
191 
192         getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad();
193     }
194 
195     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 
hasFrequents()231     public boolean hasFrequents() {
232         if (mContactTileAdapter == null) return false;
233         return mContactTileAdapter.getNumFrequents() > 0;
234     }
235 
setEmptyViewVisibility(final boolean visible)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
onStart()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
onItemClick(AdapterView<?> parent, View view, int position, long id)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      */
saveOffsets(int removedItemHeight)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      */
animateGridView(final long... idsInPlace)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 
containsId(long[] ids, long target)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
onDataSetChangedForAnimation(long... idsInPlace)424     public void onDataSetChangedForAnimation(long... idsInPlace) {
425         animateGridView(idsInPlace);
426     }
427 
428     @Override
cacheOffsetsForDatasetChange()429     public void cacheOffsetsForDatasetChange() {
430         saveOffsets(0);
431     }
432 
getListView()433     public AbsListView getListView() {
434         return mListView;
435     }
436 }
437