Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2014 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 androidx.leanback.widget;
     18 
     19 import android.animation.ArgbEvaluator;
     20 import android.animation.ValueAnimator;
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Color;
     25 import android.graphics.Rect;
     26 import android.graphics.drawable.Drawable;
     27 import android.graphics.drawable.GradientDrawable;
     28 import android.util.AttributeSet;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.widget.FrameLayout;
     32 import android.widget.ImageView;
     33 
     34 import androidx.annotation.ColorInt;
     35 import androidx.core.view.ViewCompat;
     36 import androidx.leanback.R;
     37 
     38 /**
     39  * <p>A widget that draws a search affordance, represented by a round background and an icon.</p>
     40  *
     41  * The background color and icon can be customized.
     42  */
     43 public class SearchOrbView extends FrameLayout implements View.OnClickListener {
     44     private OnClickListener mListener;
     45     private View mRootView;
     46     private View mSearchOrbView;
     47     private ImageView mIcon;
     48     private Drawable mIconDrawable;
     49     private Colors mColors;
     50     private final float mFocusedZoom;
     51     private final int mPulseDurationMs;
     52     private final int mScaleDurationMs;
     53     private final float mUnfocusedZ;
     54     private final float mFocusedZ;
     55     private ValueAnimator mColorAnimator;
     56     private boolean mColorAnimationEnabled;
     57     private boolean mAttachedToWindow;
     58 
     59     /**
     60      * A set of colors used to display the search orb.
     61      */
     62     public static class Colors {
     63         private static final float BRIGHTNESS_ALPHA = 0.15f;
     64 
     65         /**
     66          * Constructs a color set using the given color for the search orb.
     67          * Other colors are provided by the framework.
     68          *
     69          * @param color The main search orb color.
     70          */
     71         public Colors(@ColorInt int color) {
     72             this(color, color);
     73         }
     74 
     75         /**
     76          * Constructs a color set using the given colors for the search orb.
     77          * Other colors are provided by the framework.
     78          *
     79          * @param color The main search orb color.
     80          * @param brightColor A brighter version of the search orb used for animation.
     81          */
     82         public Colors(@ColorInt int color, @ColorInt int brightColor) {
     83             this(color, brightColor, Color.TRANSPARENT);
     84         }
     85 
     86         /**
     87          * Constructs a color set using the given colors.
     88          *
     89          * @param color The main search orb color.
     90          * @param brightColor A brighter version of the search orb used for animation.
     91          * @param iconColor A color used to tint the search orb icon.
     92          */
     93         public Colors(@ColorInt int color, @ColorInt int brightColor, @ColorInt int iconColor) {
     94             this.color = color;
     95             this.brightColor = brightColor == color ? getBrightColor(color) : brightColor;
     96             this.iconColor = iconColor;
     97         }
     98 
     99         /**
    100          * The main color of the search orb.
    101          */
    102         @ColorInt
    103         public int color;
    104 
    105         /**
    106          * A brighter version of the search orb used for animation.
    107          */
    108         @ColorInt
    109         public int brightColor;
    110 
    111         /**
    112          * A color used to tint the search orb icon.
    113          */
    114         @ColorInt
    115         public int iconColor;
    116 
    117         /**
    118          * Computes a default brighter version of the given color.
    119          */
    120         public static int getBrightColor(int color) {
    121             final float brightnessValue = 0xff * BRIGHTNESS_ALPHA;
    122             int red = (int)(Color.red(color) * (1 - BRIGHTNESS_ALPHA) + brightnessValue);
    123             int green = (int)(Color.green(color) * (1 - BRIGHTNESS_ALPHA) + brightnessValue);
    124             int blue = (int)(Color.blue(color) * (1 - BRIGHTNESS_ALPHA) + brightnessValue);
    125             int alpha = (int)(Color.alpha(color) * (1 - BRIGHTNESS_ALPHA) + brightnessValue);
    126             return Color.argb(alpha, red, green, blue);
    127         }
    128     }
    129 
    130     private final ArgbEvaluator mColorEvaluator = new ArgbEvaluator();
    131 
    132     private final ValueAnimator.AnimatorUpdateListener mUpdateListener =
    133             new ValueAnimator.AnimatorUpdateListener() {
    134         @Override
    135         public void onAnimationUpdate(ValueAnimator animator) {
    136             Integer color = (Integer) animator.getAnimatedValue();
    137             setOrbViewColor(color.intValue());
    138         }
    139     };
    140 
    141     private ValueAnimator mShadowFocusAnimator;
    142 
    143     private final ValueAnimator.AnimatorUpdateListener mFocusUpdateListener =
    144             new ValueAnimator.AnimatorUpdateListener() {
    145         @Override
    146         public void onAnimationUpdate(ValueAnimator animation) {
    147             setSearchOrbZ(animation.getAnimatedFraction());
    148         }
    149     };
    150 
    151     void setSearchOrbZ(float fraction) {
    152         ViewCompat.setZ(mSearchOrbView, mUnfocusedZ + fraction * (mFocusedZ - mUnfocusedZ));
    153     }
    154 
    155     public SearchOrbView(Context context) {
    156         this(context, null);
    157     }
    158 
    159     public SearchOrbView(Context context, AttributeSet attrs) {
    160         this(context, attrs, R.attr.searchOrbViewStyle);
    161     }
    162 
    163     public SearchOrbView(Context context, AttributeSet attrs, int defStyleAttr) {
    164         super(context, attrs, defStyleAttr);
    165 
    166         final Resources res = context.getResources();
    167 
    168         LayoutInflater inflater = (LayoutInflater) context
    169                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    170         mRootView = inflater.inflate(getLayoutResourceId(), this, true);
    171         mSearchOrbView = mRootView.findViewById(R.id.search_orb);
    172         mIcon = (ImageView) mRootView.findViewById(R.id.icon);
    173 
    174         mFocusedZoom = context.getResources().getFraction(
    175                 R.fraction.lb_search_orb_focused_zoom, 1, 1);
    176         mPulseDurationMs = context.getResources().getInteger(
    177                 R.integer.lb_search_orb_pulse_duration_ms);
    178         mScaleDurationMs = context.getResources().getInteger(
    179                 R.integer.lb_search_orb_scale_duration_ms);
    180         mFocusedZ = context.getResources().getDimensionPixelSize(
    181                 R.dimen.lb_search_orb_focused_z);
    182         mUnfocusedZ = context.getResources().getDimensionPixelSize(
    183                 R.dimen.lb_search_orb_unfocused_z);
    184 
    185         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbSearchOrbView,
    186                 defStyleAttr, 0);
    187 
    188         Drawable img = a.getDrawable(R.styleable.lbSearchOrbView_searchOrbIcon);
    189         if (img == null) {
    190             img = res.getDrawable(R.drawable.lb_ic_in_app_search);
    191         }
    192         setOrbIcon(img);
    193 
    194         int defColor = res.getColor(R.color.lb_default_search_color);
    195         int color = a.getColor(R.styleable.lbSearchOrbView_searchOrbColor, defColor);
    196         int brightColor = a.getColor(
    197                 R.styleable.lbSearchOrbView_searchOrbBrightColor, color);
    198         int iconColor = a.getColor(R.styleable.lbSearchOrbView_searchOrbIconColor, Color.TRANSPARENT);
    199         setOrbColors(new Colors(color, brightColor, iconColor));
    200         a.recycle();
    201 
    202         setFocusable(true);
    203         setClipChildren(false);
    204         setOnClickListener(this);
    205         setSoundEffectsEnabled(false);
    206         setSearchOrbZ(0);
    207 
    208         // Icon has no background, but must be on top of the search orb view
    209         ViewCompat.setZ(mIcon, mFocusedZ);
    210     }
    211 
    212     int getLayoutResourceId() {
    213         return R.layout.lb_search_orb;
    214     }
    215 
    216     void scaleOrbViewOnly(float scale) {
    217         mSearchOrbView.setScaleX(scale);
    218         mSearchOrbView.setScaleY(scale);
    219     }
    220 
    221     float getFocusedZoom() {
    222         return mFocusedZoom;
    223     }
    224 
    225     @Override
    226     public void onClick(View view) {
    227         if (null != mListener) {
    228             mListener.onClick(view);
    229         }
    230     }
    231 
    232     private void startShadowFocusAnimation(boolean gainFocus, int duration) {
    233         if (mShadowFocusAnimator == null) {
    234             mShadowFocusAnimator = ValueAnimator.ofFloat(0f, 1f);
    235             mShadowFocusAnimator.addUpdateListener(mFocusUpdateListener);
    236         }
    237         if (gainFocus) {
    238             mShadowFocusAnimator.start();
    239         } else {
    240             mShadowFocusAnimator.reverse();
    241         }
    242         mShadowFocusAnimator.setDuration(duration);
    243     }
    244 
    245     @Override
    246     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
    247         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    248         animateOnFocus(gainFocus);
    249     }
    250 
    251     void animateOnFocus(boolean hasFocus) {
    252         final float zoom = hasFocus ? mFocusedZoom : 1f;
    253         mRootView.animate().scaleX(zoom).scaleY(zoom).setDuration(mScaleDurationMs).start();
    254         startShadowFocusAnimation(hasFocus, mScaleDurationMs);
    255         enableOrbColorAnimation(hasFocus);
    256     }
    257 
    258     /**
    259      * Sets the orb icon.
    260      * @param icon the drawable to be used as the icon
    261      */
    262     public void setOrbIcon(Drawable icon) {
    263         mIconDrawable = icon;
    264         mIcon.setImageDrawable(mIconDrawable);
    265     }
    266 
    267     /**
    268      * Returns the orb icon
    269      * @return the drawable used as the icon
    270      */
    271     public Drawable getOrbIcon() {
    272         return mIconDrawable;
    273     }
    274 
    275     /**
    276      * Sets the on click listener for the orb.
    277      * @param listener The listener.
    278      */
    279     public void setOnOrbClickedListener(OnClickListener listener) {
    280         mListener = listener;
    281     }
    282 
    283     /**
    284      * Sets the background color of the search orb.
    285      * Other colors will be provided by the framework.
    286      *
    287      * @param color the RGBA color
    288      */
    289     public void setOrbColor(int color) {
    290         setOrbColors(new Colors(color, color, Color.TRANSPARENT));
    291     }
    292 
    293     /**
    294      * Sets the search orb colors.
    295      * Other colors are provided by the framework.
    296      * @deprecated Use {@link #setOrbColors(Colors)} instead.
    297      */
    298     @Deprecated
    299     public void setOrbColor(@ColorInt int color, @ColorInt int brightColor) {
    300         setOrbColors(new Colors(color, brightColor, Color.TRANSPARENT));
    301     }
    302 
    303     /**
    304      * Returns the orb color
    305      * @return the RGBA color
    306      */
    307     @ColorInt
    308     public int getOrbColor() {
    309         return mColors.color;
    310     }
    311 
    312     /**
    313      * Sets the {@link Colors} used to display the search orb.
    314      */
    315     public void setOrbColors(Colors colors) {
    316         mColors = colors;
    317         mIcon.setColorFilter(mColors.iconColor);
    318 
    319         if (mColorAnimator == null) {
    320             setOrbViewColor(mColors.color);
    321         } else {
    322             enableOrbColorAnimation(true);
    323         }
    324     }
    325 
    326     /**
    327      * Returns the {@link Colors} used to display the search orb.
    328      */
    329     public Colors getOrbColors() {
    330         return mColors;
    331     }
    332 
    333     /**
    334      * Enables or disables the orb color animation.
    335      *
    336      * <p>
    337      * Orb color animation is handled automatically when the orb is focused/unfocused,
    338      * however, an app may choose to override the current animation state, for example
    339      * when an activity is paused.
    340      * </p>
    341      */
    342     public void enableOrbColorAnimation(boolean enable) {
    343         mColorAnimationEnabled = enable;
    344         updateColorAnimator();
    345     }
    346 
    347     private void updateColorAnimator() {
    348         if (mColorAnimator != null) {
    349             mColorAnimator.end();
    350             mColorAnimator = null;
    351         }
    352         if (mColorAnimationEnabled && mAttachedToWindow) {
    353             // TODO: set interpolator (material if available)
    354             mColorAnimator = ValueAnimator.ofObject(mColorEvaluator,
    355                     mColors.color, mColors.brightColor, mColors.color);
    356             mColorAnimator.setRepeatCount(ValueAnimator.INFINITE);
    357             mColorAnimator.setDuration(mPulseDurationMs * 2);
    358             mColorAnimator.addUpdateListener(mUpdateListener);
    359             mColorAnimator.start();
    360         }
    361     }
    362 
    363     void setOrbViewColor(int color) {
    364         if (mSearchOrbView.getBackground() instanceof GradientDrawable) {
    365             ((GradientDrawable) mSearchOrbView.getBackground()).setColor(color);
    366         }
    367     }
    368 
    369     @Override
    370     protected void onAttachedToWindow() {
    371         super.onAttachedToWindow();
    372         mAttachedToWindow = true;
    373         updateColorAnimator();
    374     }
    375 
    376     @Override
    377     protected void onDetachedFromWindow() {
    378         mAttachedToWindow = false;
    379         // Must stop infinite animation to prevent activity leak
    380         updateColorAnimator();
    381         super.onDetachedFromWindow();
    382     }
    383 }
    384