Home | History | Annotate | Download | only in widget
      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.widget;
     17 
     18 import com.android.contacts.format.FormatUtils;
     19 import com.android.internal.R;
     20 
     21 import android.database.CharArrayBuffer;
     22 import android.graphics.Color;
     23 import android.os.Handler;
     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, TextWithHighlightingFactory {
     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 TextWithHighlightingImpl implements TextWithHighlighting {
     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 TextWithHighlightingImpl() {
     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 = FormatUtils.overlapPoint(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         @SuppressWarnings("unchecked")
     88         public <T> T[] getSpans(int start, int end, Class<T> type) {
     89             if (mDimmingEnabled) {
     90                 return (T[])mSpans;
     91             } else {
     92                 return (T[])sEmptySpans;
     93             }
     94         }
     95 
     96         public int getSpanStart(Object tag) {
     97             // We only have one span - no need to check the tag parameter
     98             return mDimmingSpanStart;
     99         }
    100 
    101         public int getSpanEnd(Object tag) {
    102             // We only have one span - no need to check the tag parameter
    103             return mDimmingSpanEnd;
    104         }
    105 
    106         public int getSpanFlags(Object tag) {
    107             // String is immutable - flags not needed
    108             return 0;
    109         }
    110 
    111         public int nextSpanTransition(int start, int limit, Class type) {
    112             // Never called since we only have one span
    113             return 0;
    114         }
    115 
    116         public char charAt(int index) {
    117             return mText.data[index];
    118         }
    119 
    120         public int length() {
    121             return mText.sizeCopied;
    122         }
    123 
    124         public CharSequence subSequence(int start, int end) {
    125             // Never called - implementing for completeness
    126             return new String(mText.data, start, end);
    127         }
    128 
    129         @Override
    130         public String toString() {
    131             return mString;
    132         }
    133     }
    134 
    135     /**
    136      * A Span that modifies alpha of the default foreground color.
    137      */
    138     private static class DimmingSpan extends CharacterStyle {
    139         private int mAlpha;
    140 
    141         public void setAlpha(int alpha) {
    142             mAlpha = alpha;
    143         }
    144 
    145         @Override
    146         public void updateDrawState(TextPaint ds) {
    147 
    148             // Only dim the text in the basic state; not selected, focused or pressed
    149             int[] states = ds.drawableState;
    150             if (states != null) {
    151                 int count = states.length;
    152                 for (int i = 0; i < count; i++) {
    153                     switch (states[i]) {
    154                         case R.attr.state_pressed:
    155                         case R.attr.state_selected:
    156                         case R.attr.state_focused:
    157                             // We can simply return, because the supplied text
    158                             // paint is already configured with defaults.
    159                             return;
    160                     }
    161                 }
    162             }
    163 
    164             int color = ds.getColor();
    165             color = Color.argb(mAlpha, Color.red(color), Color.green(color), Color.blue(color));
    166             ds.setColor(color);
    167         }
    168     }
    169 
    170     /**
    171      * Constructor.
    172      */
    173     public TextHighlightingAnimation(int duration) {
    174         mDuration = duration;
    175         mHandler = new Handler();
    176         mDimmingSpan = new DimmingSpan();
    177         mDimmingSpan.setAlpha(MAX_ALPHA);
    178     }
    179 
    180     /**
    181      * Returns a Spanned that can be used by a text view to show text with highlighting.
    182      */
    183     public TextWithHighlightingImpl createTextWithHighlighting() {
    184         return new TextWithHighlightingImpl();
    185     }
    186 
    187     /**
    188      * Override and invalidate (redraw) TextViews showing {@link TextWithHighlightingImpl}.
    189      */
    190     protected abstract void invalidate();
    191 
    192     /**
    193      * Starts the highlighting animation, which will dim portions of text.
    194      */
    195     public void startHighlighting() {
    196         startAnimation(true);
    197     }
    198 
    199     /**
    200      * Starts un-highlighting animation, which will brighten the dimmed portions of text
    201      * to the brightness level of the rest of text.
    202      */
    203     public void stopHighlighting() {
    204         startAnimation(false);
    205     }
    206 
    207     /**
    208      * Called when the animation starts.
    209      */
    210     protected void onAnimationStarted() {
    211     }
    212 
    213     /**
    214      * Called when the animation has stopped.
    215      */
    216     protected void onAnimationEnded() {
    217     }
    218 
    219     private void startAnimation(boolean dim) {
    220         if (mDimming != dim) {
    221             mDimming = dim;
    222             long now = System.currentTimeMillis();
    223             if (!mAnimating) {
    224                 mAnimating = true;
    225                 mTargetTime = now + mDuration;
    226                 onAnimationStarted();
    227                 mHandler.post(this);
    228             } else  {
    229 
    230                 // If we have started dimming, reverse the direction and adjust the target
    231                 // time accordingly.
    232                 mTargetTime = (now + mDuration) - (mTargetTime - now);
    233             }
    234         }
    235     }
    236 
    237     /**
    238      * Animation step.
    239      */
    240     public void run() {
    241         long now = System.currentTimeMillis();
    242         long timeLeft = mTargetTime - now;
    243         if (timeLeft < 0) {
    244             mDimmingSpan.setAlpha(mDimming ? MIN_ALPHA : MAX_ALPHA);
    245             mAnimating = false;
    246             onAnimationEnded();
    247             return;
    248         }
    249 
    250         // Start=1, end=0
    251         float virtualTime = (float)timeLeft / mDuration;
    252         if (mDimming) {
    253             float interpolatedTime = DECELERATE_INTERPOLATOR.getInterpolation(virtualTime);
    254             mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * interpolatedTime));
    255         } else {
    256             float interpolatedTime = ACCELERATE_INTERPOLATOR.getInterpolation(virtualTime);
    257             mDimmingSpan.setAlpha((int)(MIN_ALPHA + (MAX_ALPHA-MIN_ALPHA) * (1-interpolatedTime)));
    258         }
    259 
    260         invalidate();
    261 
    262         // Repeat
    263         mHandler.postDelayed(this, FRAME_RATE);
    264     }
    265 }
    266