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