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