Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2010 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.contacts;
     17 
     18 import com.android.internal.R;
     19 
     20 import android.database.CharArrayBuffer;
     21 import android.graphics.Color;
     22 import android.os.Handler;
     23 import android.text.Spanned;
     24 import android.text.TextPaint;
     25 import android.text.style.CharacterStyle;
     26 import android.view.animation.AccelerateInterpolator;
     27 import android.view.animation.DecelerateInterpolator;
     28 
     29 /**
     30  * An animation that alternately dims and brightens the non-highlighted portion of text.
     31  */
     32 public abstract class TextHighlightingAnimation implements Runnable {
     33 
     34     private static final int MAX_ALPHA = 255;
     35     private static final int MIN_ALPHA = 50;
     36 
     37     private AccelerateInterpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
     38     private DecelerateInterpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
     39 
     40     private final static DimmingSpan[] sEmptySpans = new DimmingSpan[0];
     41 
     42     /**
     43      * Frame rate expressed a number of millis between frames.
     44      */
     45     private static final long FRAME_RATE = 50;
     46 
     47     private DimmingSpan mDimmingSpan;
     48     private Handler mHandler;
     49     private boolean mAnimating;
     50     private boolean mDimming;
     51     private long mTargetTime;
     52     private final int mDuration;
     53 
     54     /**
     55      * A Spanned that highlights a part of text by dimming another part of that text.
     56      */
     57     public class TextWithHighlighting implements Spanned {
     58 
     59         private final DimmingSpan[] mSpans;
     60         private boolean mDimmingEnabled;
     61         private CharArrayBuffer mText;
     62         private int mDimmingSpanStart;
     63         private int mDimmingSpanEnd;
     64         private String mString;
     65 
     66         public TextWithHighlighting() {
     67             mSpans = new DimmingSpan[] { mDimmingSpan };
     68         }
     69 
     70         public void setText(CharArrayBuffer baseText, CharArrayBuffer highlightedText) {
     71             mText = baseText;
     72 
     73             // TODO figure out a way to avoid string allocation
     74             mString = new String(mText.data, 0, mText.sizeCopied);
     75 
     76             int index = indexOf(baseText, highlightedText);
     77 
     78             if (index == 0 || index == -1) {
     79                 mDimmingEnabled = false;
     80             } else {
     81                 mDimmingEnabled = true;
     82                 mDimmingSpanStart = 0;
     83                 mDimmingSpanEnd = index;
     84             }
     85         }
     86 
     87         /**
     88          * An implementation of indexOf on CharArrayBuffers that finds the first match of
     89          * the start of buffer2 in buffer1.  For example, indexOf("abcd", "cdef") == 2
     90          */
     91         private int indexOf(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
     92             char[] string1 = buffer1.data;
     93             char[] string2 = buffer2.data;
     94             int count1 = buffer1.sizeCopied;
     95             int count2 = buffer2.sizeCopied;
     96 
     97             // Ignore matching tails of the two buffers
     98             while (count1 > 0 && count2 > 0 && string1[count1 - 1] == string2[count2 - 1]) {
     99                 count1--;
    100                 count2--;
    101             }
    102 
    103             int size = count2;
    104             for (int i = 0; i < count1; i++) {
    105                 if (i + size > count1) {
    106                     size = count1 - i;
    107                 }
    108                 int j;
    109                 for (j = 0; j < size; j++) {
    110                     if (string1[i+j] != string2[j]) {
    111                         break;
    112                     }
    113                 }
    114                 if (j == size) {
    115                     return i;
    116                 }
    117             }
    118 
    119             return -1;
    120         }
    121 
    122 
    123         @SuppressWarnings("unchecked")
    124         public <T> T[] getSpans(int start, int end, Class<T> type) {
    125             if (mDimmingEnabled) {
    126                 return (T[])mSpans;
    127             } else {
    128                 return (T[])sEmptySpans;
    129             }
    130         }
    131 
    132         public int getSpanStart(Object tag) {
    133             // We only have one span - no need to check the tag parameter
    134             return mDimmingSpanStart;
    135         }
    136 
    137         public int getSpanEnd(Object tag) {
    138             // We only have one span - no need to check the tag parameter
    139             return mDimmingSpanEnd;
    140         }
    141 
    142         public int getSpanFlags(Object tag) {
    143             // String is immutable - flags not needed
    144             return 0;
    145         }
    146 
    147         public int nextSpanTransition(int start, int limit, Class type) {
    148             // Never called since we only have one span
    149             return 0;
    150         }
    151 
    152         public char charAt(int index) {
    153             return mText.data[index];
    154         }
    155 
    156         public int length() {
    157             return mText.sizeCopied;
    158         }
    159 
    160         public CharSequence subSequence(int start, int end) {
    161             // Never called - implementing for completeness
    162             return new String(mText.data, start, end);
    163         }
    164 
    165         @Override
    166         public String toString() {
    167             return mString;
    168         }
    169     }
    170 
    171     /**
    172      * A Span that modifies alpha of the default foreground color.
    173      */
    174     private static class DimmingSpan extends CharacterStyle {
    175         private int mAlpha;
    176 
    177         public void setAlpha(int alpha) {
    178             mAlpha = alpha;
    179         }
    180 
    181         @Override
    182         public void updateDrawState(TextPaint ds) {
    183 
    184             // Only dim the text in the basic state; not selected, focused or pressed
    185             int[] states = ds.drawableState;
    186             if (states != null) {
    187                 int count = states.length;
    188                 for (int i = 0; i < count; i++) {
    189                     switch (states[i]) {
    190                         case R.attr.state_pressed:
    191                         case R.attr.state_selected:
    192                         case R.attr.state_focused:
    193                             // We can simply return, because the supplied text
    194                             // paint is already configured with defaults.
    195                             return;
    196                     }
    197                 }
    198             }
    199 
    200             int color = ds.getColor();
    201             color = Color.argb(mAlpha, Color.red(color), Color.green(color), Color.blue(color));
    202             ds.setColor(color);
    203         }
    204     }
    205 
    206     /**
    207      * Constructor.
    208      */
    209     public TextHighlightingAnimation(int duration) {
    210         mDuration = duration;
    211         mHandler = new Handler();
    212         mDimmingSpan = new DimmingSpan();
    213         mDimmingSpan.setAlpha(MAX_ALPHA);
    214     }
    215 
    216     /**
    217      * Returns a Spanned that can be used by a text view to show text with highlighting.
    218      */
    219     public TextWithHighlighting createTextWithHighlighting() {
    220         return new TextWithHighlighting();
    221     }
    222 
    223     /**
    224      * Override and invalidate (redraw) TextViews showing {@link TextWithHighlighting}.
    225      */
    226     protected abstract void invalidate();
    227 
    228     /**
    229      * Starts the highlighting animation, which will dim portions of text.
    230      */
    231     public void startHighlighting() {
    232         startAnimation(true);
    233     }
    234 
    235     /**
    236      * Starts un-highlighting animation, which will brighten the dimmed portions of text
    237      * to the brightness level of the rest of text.
    238      */
    239     public void stopHighlighting() {
    240         startAnimation(false);
    241     }
    242 
    243     /**
    244      * Called when the animation starts.
    245      */
    246     protected void onAnimationStarted() {
    247     }
    248 
    249     /**
    250      * Called when the animation has stopped.
    251      */
    252     protected void onAnimationEnded() {
    253     }
    254 
    255     private void startAnimation(boolean dim) {
    256         if (mDimming != dim) {
    257             mDimming = dim;
    258             long now = System.currentTimeMillis();
    259             if (!mAnimating) {
    260                 mAnimating = true;
    261                 mTargetTime = now + mDuration;
    262                 onAnimationStarted();
    263                 mHandler.post(this);
    264             } else  {
    265 
    266                 // If we have started dimming, reverse the direction and adjust the target
    267                 // time accordingly.
    268                 mTargetTime = (now + mDuration) - (mTargetTime - now);
    269             }
    270         }
    271     }
    272 
    273     /**
    274      * Animation step.
    275      */
    276     public void run() {
    277         long now = System.currentTimeMillis();
    278         long timeLeft = mTargetTime - now;
    279         if (timeLeft < 0) {
    280             mDimmingSpan.setAlpha(mDimming ? MIN_ALPHA : MAX_ALPHA);
    281             mAnimating = false;
    282             onAnimationEnded();
    283             return;
    284         }
    285 
    286         // Start=1, end=0
    287         float virtualTime = (float)timeLeft / mDuration;
    288         if (mDimming) {
    289             float interpolatedTime = DECELERATE_INTERPOLATOR.getInterpolation(virtualTime);
    290             mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * interpolatedTime));
    291         } else {
    292             float interpolatedTime = ACCELERATE_INTERPOLATOR.getInterpolation(virtualTime);
    293             mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * (1-interpolatedTime)));
    294         }
    295 
    296         invalidate();
    297 
    298         // Repeat
    299         mHandler.postDelayed(this, FRAME_RATE);
    300     }
    301 }
    302