Home | History | Annotate | Download | only in list
      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