Home | History | Annotate | Download | only in dialpad
      1 /*
      2  * Copyright (C) 2012 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 
     17 package com.android.dialer.dialpad;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.text.Spannable;
     22 import android.text.SpannableString;
     23 import android.text.TextUtils;
     24 import android.text.style.ForegroundColorSpan;
     25 import android.view.LayoutInflater;
     26 import android.view.View;
     27 import android.view.View.OnClickListener;
     28 import android.view.ViewGroup;
     29 import android.view.View.OnLongClickListener;
     30 import android.view.animation.AccelerateDecelerateInterpolator;
     31 import android.view.animation.DecelerateInterpolator;
     32 import android.view.animation.Interpolator;
     33 import android.view.animation.LinearInterpolator;
     34 import android.view.animation.OvershootInterpolator;
     35 import android.widget.LinearLayout;
     36 import android.widget.TextView;
     37 
     38 import com.android.dialer.R;
     39 
     40 import com.google.common.collect.Lists;
     41 
     42 import java.util.List;
     43 
     44 /**
     45 * This class controls the display and animation logic behind the smart dialing suggestion strip.
     46 *
     47 * It allows a list of SmartDialEntries to be assigned to the suggestion strip via
     48 * {@link #setEntries}, and also animates the removal of old suggestions.
     49 *
     50 * To avoid creating new views every time new entries are assigned, references to 2 *
     51 * {@link #NUM_SUGGESTIONS} views are kept in {@link #mViews} and {@link #mViewOverlays}.
     52 *
     53 * {@code mViews} contains the active views that are currently being displayed to the user,
     54 * while {@code mViewOverlays} contains the views that are used as view overlays. The view
     55 * overlays are used to provide the illusion of the former suggestions fading out. These two
     56 * lists of views are rotated each time a new set of entries is assigned to achieve the appropriate
     57 * cross fade animations using the new {@link View#getOverlay()} API.
     58 */
     59 public class SmartDialController {
     60     public static final String LOG_TAG = "SmartDial";
     61 
     62     /**
     63      * Handtuned interpolator used to achieve the bounce effect when suggestions slide up. It
     64      * uses a combination of a decelerate interpolator and overshoot interpolator to first
     65      * decelerate, and then overshoot its top bounds and bounce back to its final position.
     66      */
     67     private class DecelerateAndOvershootInterpolator implements Interpolator {
     68         private DecelerateInterpolator a;
     69         private OvershootInterpolator b;
     70 
     71         public DecelerateAndOvershootInterpolator() {
     72             a = new DecelerateInterpolator(1.5f);
     73             b = new OvershootInterpolator(1.3f);
     74         }
     75 
     76         @Override
     77         public float getInterpolation(float input) {
     78             if (input > 0.6) {
     79                 return b.getInterpolation(input);
     80             } else {
     81                 return a.getInterpolation(input);
     82             }
     83         }
     84 
     85     }
     86 
     87     private DecelerateAndOvershootInterpolator mDecelerateAndOvershootInterpolator =
     88             new DecelerateAndOvershootInterpolator();
     89     private AccelerateDecelerateInterpolator mAccelerateDecelerateInterpolator =
     90             new AccelerateDecelerateInterpolator();
     91 
     92     private List<SmartDialEntry> mEntries;
     93     private List<SmartDialEntry> mOldEntries;
     94 
     95     private final int mNameHighlightedTextColor;
     96     private final int mNumberHighlightedTextColor;
     97 
     98     private final LinearLayout mList;
     99     private final View mBackground;
    100 
    101     private final List<LinearLayout> mViewOverlays = Lists.newArrayList();
    102     private final List<LinearLayout> mViews = Lists.newArrayList();
    103 
    104     private static final int NUM_SUGGESTIONS = 3;
    105 
    106     private static final long ANIM_DURATION = 200;
    107 
    108     private static final float BACKGROUND_FADE_AMOUNT = 0.25f;
    109 
    110     Resources mResources;
    111 
    112     public SmartDialController(Context context, ViewGroup parent,
    113             OnClickListener shortClickListener, OnLongClickListener longClickListener) {
    114         final Resources res = context.getResources();
    115         mResources = res;
    116 
    117         mNameHighlightedTextColor = res.getColor(R.color.smartdial_name_highlighted_text_color);
    118         mNumberHighlightedTextColor = res.getColor(
    119                 R.color.smartdial_number_highlighted_text_color);
    120 
    121         mList = (LinearLayout) parent.findViewById(R.id.dialpad_smartdial_list);
    122         mBackground = parent.findViewById(R.id.dialpad_smartdial_list_background);
    123 
    124         mEntries = Lists.newArrayList();
    125         for (int i = 0; i < NUM_SUGGESTIONS; i++) {
    126             mEntries.add(SmartDialEntry.NULL);
    127         }
    128 
    129         mOldEntries = mEntries;
    130 
    131         final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
    132                 Context.LAYOUT_INFLATER_SERVICE);
    133 
    134         for (int i = 0; i < NUM_SUGGESTIONS * 2; i++) {
    135             final LinearLayout view = (LinearLayout) inflater.inflate(
    136                     R.layout.dialpad_smartdial_item, mList, false);
    137             view.setOnClickListener(shortClickListener);
    138             view.setOnLongClickListener(longClickListener);
    139             if (i < NUM_SUGGESTIONS) {
    140                 mViews.add(view);
    141             } else {
    142                 mViewOverlays.add(view);
    143             }
    144             // Add all the views to mList so that they can get measured properly for animation
    145             // purposes. Once setEntries is called they will be removed and added as appropriate.
    146             view.setEnabled(false);
    147             mList.addView(view);
    148         }
    149     }
    150 
    151     /** Remove all entries. */
    152     public void clear() {
    153         mOldEntries = mEntries;
    154         mEntries = Lists.newArrayList();
    155         for (int i = 0; i < NUM_SUGGESTIONS; i++) {
    156             mEntries.add(SmartDialEntry.NULL);
    157         }
    158         updateViews();
    159     }
    160 
    161     /** Set entries. At the end of this method {@link #mEntries} should contain exactly
    162      *  {@link #NUM_SUGGESTIONS} entries.*/
    163     public void setEntries(List<SmartDialEntry> entries) {
    164         if (entries == null) throw new IllegalArgumentException();
    165         mOldEntries = mEntries;
    166         mEntries = entries;
    167 
    168         final int size = mEntries.size();
    169         if (size <= 1) {
    170             if (size == 0) {
    171                 mEntries.add(SmartDialEntry.NULL);
    172             }
    173             // add a null entry to push the single entry into the middle
    174             mEntries.add(0, SmartDialEntry.NULL);
    175         } else if (size >= 2) {
    176             // swap the 1st and 2nd entries so that the highest confidence match goes into the
    177             // middle
    178             swap(0, 1);
    179         }
    180 
    181         while (mEntries.size() < NUM_SUGGESTIONS) {
    182             mEntries.add(SmartDialEntry.NULL);
    183         }
    184 
    185         updateViews();
    186     }
    187 
    188     /**
    189      * This method is called every time a new set of SmartDialEntries is to be assigned to the
    190      * suggestions view. The current set of active views are to be used as view overlays and
    191      * faded out, while the former view overlays are assigned the current entries, added to
    192      * {@link #mList} and faded into view.
    193      */
    194     private void updateViews() {
    195         // Remove all views from the root in preparation to swap the two sets of views
    196         mList.removeAllViews();
    197         try {
    198             mList.getOverlay().clear();
    199         } catch (NullPointerException e) {
    200             // Catch possible NPE b/8895794
    201         }
    202 
    203         // Used to track whether or not to animate the overlay. In the case where the suggestion
    204         // at position i will slide from the left or right, or if the suggestion at position i
    205         // has not changed, the overlay at i should be hidden immediately. Overlay animations are
    206         // set in a separate loop from the active views to avoid unnecessarily reanimating the same
    207         // overlay multiple times.
    208         boolean[] dontAnimateOverlay = new boolean[NUM_SUGGESTIONS];
    209         boolean noSuggestions = true;
    210 
    211         // At this point in time {@link #mViews} contains the former active views with old
    212         // suggestions that will be swapped out to serve as view overlays, while
    213         // {@link #mViewOverlays} contains the former overlays that will now serve as active
    214         // views.
    215         for (int i = 0; i < NUM_SUGGESTIONS; i++) {
    216             // Retrieve the former overlay to be used as the new active view
    217             final LinearLayout active = mViewOverlays.get(i);
    218             final SmartDialEntry item = mEntries.get(i);
    219 
    220             noSuggestions &= (item == SmartDialEntry.NULL);
    221 
    222             assignEntryToView(active, mEntries.get(i));
    223             final SmartDialEntry oldItem = mOldEntries.get(i);
    224             // The former active view will now be used as an overlay for the cross-fade effect
    225             final LinearLayout overlay = mViews.get(i);
    226             show(active);
    227             if (!containsSameContact(oldItem, item)) {
    228                 // Determine what kind of animation to use for the new view
    229                 if (i == 1) { // Middle suggestion
    230                     if (containsSameContact(item, mOldEntries.get(0))) {
    231                         // Suggestion went from the left to the middle, slide it left to right
    232                         animateSlideFromLeft(active);
    233                         dontAnimateOverlay[0] = true;
    234                     } else if (containsSameContact(item, mOldEntries.get(2))) {
    235                         // Suggestion sent from the right to the middle, slide it right to left
    236                         animateSlideFromRight(active);
    237                         dontAnimateOverlay[2] = true;
    238                     } else {
    239                         animateFadeInAndSlideUp(active);
    240                     }
    241                 } else { // Left/Right suggestion
    242                     if (i == 2 && containsSameContact(item, mOldEntries.get(1))) {
    243                         // Suggestion went from middle to the right, slide it left to right
    244                         animateSlideFromLeft(active);
    245                         dontAnimateOverlay[1] = true;
    246                     } else if (i == 0 && containsSameContact(item, mOldEntries.get(1))) {
    247                         // Suggestion went from middle to the left, slide it right to left
    248                         animateSlideFromRight(active);
    249                         dontAnimateOverlay[1] = true;
    250                     } else {
    251                         animateFadeInAndSlideUp(active);
    252                     }
    253                 }
    254             } else {
    255                 // Since the same item is in the same spot, don't do any animations and just
    256                 // show the new view.
    257                 dontAnimateOverlay[i] = true;
    258             }
    259             mList.getOverlay().add(overlay);
    260             mList.addView(active);
    261             // Keep track of active views and view overlays
    262             mViews.set(i, active);
    263             mViewOverlays.set(i, overlay);
    264         }
    265 
    266         // Separate loop for overlay animations. At this point in time {@link #mViewOverlays}
    267         // contains the actual overlays.
    268         for (int i = 0; i < NUM_SUGGESTIONS; i++) {
    269             final LinearLayout overlay = mViewOverlays.get(i);
    270             if (!dontAnimateOverlay[i]) {
    271                 animateFadeOutAndSlideDown(overlay);
    272             } else {
    273                 hide(overlay);
    274             }
    275         }
    276 
    277         // Fade out the background to 25% opacity if there are suggestions. If there are no
    278         // suggestions, display the background as usual.
    279         mBackground.animate().withLayer().alpha(noSuggestions ? 1.0f : BACKGROUND_FADE_AMOUNT);
    280     }
    281 
    282     private void show(View view) {
    283         view.animate().cancel();
    284         view.setAlpha(1);
    285         view.setTranslationX(0);
    286         view.setTranslationY(0);
    287     }
    288 
    289     private void hide(View view) {
    290         view.animate().cancel();
    291         view.setAlpha(0);
    292     }
    293 
    294     private void animateFadeInAndSlideUp(View view) {
    295         view.animate().cancel();
    296         view.setAlpha(0.2f);
    297         view.setTranslationY(view.getHeight());
    298         view.animate().withLayer().alpha(1).translationY(0).setDuration(ANIM_DURATION).
    299                 setInterpolator(mDecelerateAndOvershootInterpolator);
    300     }
    301 
    302     private void animateFadeOutAndSlideDown(View view) {
    303         view.animate().cancel();
    304         view.setAlpha(1);
    305         view.setTranslationY(0);
    306         view.animate().withLayer().alpha(0).translationY(view.getHeight()).setDuration(
    307                 ANIM_DURATION).setInterpolator(mAccelerateDecelerateInterpolator);
    308     }
    309 
    310     private void animateSlideFromLeft(View view) {
    311         view.animate().cancel();
    312         view.setAlpha(1);
    313         view.setTranslationX(-1 * view.getWidth());
    314         view.animate().withLayer().translationX(0).setDuration(ANIM_DURATION).setInterpolator(
    315                 mAccelerateDecelerateInterpolator);
    316     }
    317 
    318     private void animateSlideFromRight(View view) {
    319         view.animate().cancel();
    320         view.setAlpha(1);
    321         view.setTranslationX(view.getWidth());
    322         view.animate().withLayer().translationX(0).setDuration(ANIM_DURATION).setInterpolator(
    323                 mAccelerateDecelerateInterpolator);
    324     }
    325 
    326     // Swaps the items in pos1 and pos2 of mEntries
    327     private void swap(int pos1, int pos2) {
    328         if (pos1 == pos2) {
    329             return;
    330         }
    331         final SmartDialEntry temp = mEntries.get(pos1);
    332         mEntries.set(pos1, mEntries.get(pos2));
    333         mEntries.set(pos2, temp);
    334     }
    335 
    336     // Returns whether two SmartDialEntries contain the same contact
    337     private boolean containsSameContact(SmartDialEntry x, SmartDialEntry y) {
    338         return x.contactUri.equals(y.contactUri);
    339     }
    340 
    341     // Sets the information within a SmartDialEntry to the provided view
    342     private void assignEntryToView(LinearLayout view, SmartDialEntry item) {
    343         final TextView nameView = (TextView) view.findViewById(R.id.contact_name);
    344 
    345         final TextView numberView = (TextView) view.findViewById(
    346                 R.id.contact_number);
    347 
    348         if (item == SmartDialEntry.NULL) {
    349             // Clear the text in case the view was reused.
    350             nameView.setText("");
    351             numberView.setText("");
    352             view.setEnabled(false);
    353             return;
    354         }
    355 
    356         // Highlight the display name with the provided match positions
    357         if (!TextUtils.isEmpty(item.displayName)) {
    358             final SpannableString displayName = new SpannableString(item.displayName);
    359             for (final SmartDialMatchPosition p : item.matchPositions) {
    360                 if (p.start < p.end) {
    361                     if (p.end > displayName.length()) {
    362                         p.end = displayName.length();
    363                     }
    364                     // Create a new ForegroundColorSpan for each section of the name to highlight,
    365                     // otherwise multiple highlights won't work.
    366                     displayName.setSpan(new ForegroundColorSpan(mNameHighlightedTextColor), p.start,
    367                             p.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    368                 }
    369             }
    370             nameView.setText(displayName);
    371         }
    372 
    373         // Highlight the phone number with the provided match positions
    374         if (!TextUtils.isEmpty(item.phoneNumber)) {
    375             final SmartDialMatchPosition p = item.phoneNumberMatchPosition;
    376             final SpannableString phoneNumber = new SpannableString(item.phoneNumber);
    377             if (p != null && p.start < p.end) {
    378                 if (p.end > phoneNumber.length()) {
    379                     p.end = phoneNumber.length();
    380                 }
    381                 phoneNumber.setSpan(new ForegroundColorSpan(mNumberHighlightedTextColor), p.start,
    382                         p.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    383             }
    384             numberView.setText(phoneNumber);
    385         }
    386         view.setEnabled(true);
    387         view.setTag(item);
    388     }
    389 }
    390