1/*
2 * Copyright (C) 2011 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 com.android.launcher3;
18
19import android.animation.ObjectAnimator;
20import android.animation.PropertyValuesHolder;
21import android.content.Context;
22import android.graphics.Canvas;
23import android.util.AttributeSet;
24import android.util.Pair;
25import android.view.View;
26
27import com.android.launcher3.util.Thunk;
28
29public class FocusIndicatorView extends View implements View.OnFocusChangeListener {
30
31    // It can be any number >0. The view is resized using scaleX and scaleY.
32    static final int DEFAULT_LAYOUT_SIZE = 100;
33
34    private static final float MIN_VISIBLE_ALPHA = 0.2f;
35    private static final long ANIM_DURATION = 150;
36
37    private final int[] mIndicatorPos = new int[2];
38    private final int[] mTargetViewPos = new int[2];
39
40    private ObjectAnimator mCurrentAnimation;
41    private ViewAnimState mTargetState;
42
43    private View mLastFocusedView;
44    private boolean mInitiated;
45    private final OnFocusChangeListener mHideIndicatorOnFocusListener;
46
47    private Pair<View, Boolean> mPendingCall;
48
49    public FocusIndicatorView(Context context) {
50        this(context, null);
51    }
52
53    public FocusIndicatorView(Context context, AttributeSet attrs) {
54        super(context, attrs);
55        setAlpha(0);
56        setBackgroundColor(getResources().getColor(R.color.focused_background));
57
58        mHideIndicatorOnFocusListener = new OnFocusChangeListener() {
59            @Override
60            public void onFocusChange(View v, boolean hasFocus) {
61                if (hasFocus) {
62                    endCurrentAnimation();
63                    setAlpha(0);
64                }
65            }
66        };
67    }
68
69    @Override
70    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
71        super.onSizeChanged(w, h, oldw, oldh);
72
73        // Redraw if it is already showing. This avoids a bug where the height changes by a small
74        // amount on connecting/disconnecting a bluetooth keyboard.
75        if (mLastFocusedView != null) {
76            mPendingCall = Pair.create(mLastFocusedView, Boolean.TRUE);
77            invalidate();
78        }
79    }
80
81    /**
82     * Sets the alpha of this FocusIndicatorView to 0 when a view with this listener receives focus.
83     */
84    public View.OnFocusChangeListener getHideIndicatorOnFocusListener() {
85        return mHideIndicatorOnFocusListener;
86    }
87
88    @Override
89    public void onFocusChange(View v, boolean hasFocus) {
90        mPendingCall = null;
91        if (!mInitiated && (getWidth() == 0)) {
92            // View not yet laid out. Wait until the view is ready to be drawn, so that be can
93            // get the location on screen.
94            mPendingCall = Pair.create(v, hasFocus);
95            invalidate();
96            return;
97        }
98
99        if (!mInitiated) {
100            // The parent view should always the a parent of the target view.
101            computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos);
102            mInitiated = true;
103        }
104
105        if (hasFocus) {
106            int indicatorWidth = getWidth();
107            int indicatorHeight = getHeight();
108
109            endCurrentAnimation();
110            ViewAnimState nextState = new ViewAnimState();
111            nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth;
112            nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight;
113
114            computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos);
115            nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2;
116            nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2;
117
118            if (getAlpha() > MIN_VISIBLE_ALPHA) {
119                mTargetState = nextState;
120                mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
121                        PropertyValuesHolder.ofFloat(View.ALPHA, 1),
122                        PropertyValuesHolder.ofFloat(View.TRANSLATION_X, mTargetState.x),
123                        PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, mTargetState.y),
124                        PropertyValuesHolder.ofFloat(View.SCALE_X, mTargetState.scaleX),
125                        PropertyValuesHolder.ofFloat(View.SCALE_Y, mTargetState.scaleY));
126            } else {
127                applyState(nextState);
128                mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
129                        PropertyValuesHolder.ofFloat(View.ALPHA, 1));
130            }
131            mLastFocusedView = v;
132        } else {
133            if (mLastFocusedView == v) {
134                mLastFocusedView = null;
135                endCurrentAnimation();
136                mCurrentAnimation = LauncherAnimUtils.ofPropertyValuesHolder(this,
137                        PropertyValuesHolder.ofFloat(View.ALPHA, 0));
138            }
139        }
140        if (mCurrentAnimation != null) {
141            mCurrentAnimation.setDuration(ANIM_DURATION).start();
142        }
143    }
144
145    private void endCurrentAnimation() {
146        if (mCurrentAnimation != null) {
147            mCurrentAnimation.cancel();
148            mCurrentAnimation = null;
149        }
150        if (mTargetState != null) {
151            applyState(mTargetState);
152            mTargetState = null;
153        }
154    }
155
156    private void applyState(ViewAnimState state) {
157        setTranslationX(state.x);
158        setTranslationY(state.y);
159        setScaleX(state.scaleX);
160        setScaleY(state.scaleY);
161    }
162
163    @Override
164    protected void onDraw(Canvas canvas) {
165        if (mPendingCall != null) {
166            onFocusChange(mPendingCall.first, mPendingCall.second);
167        }
168    }
169
170    /**
171     * Computes the location of a view relative to {@param parent}, off-setting
172     * any shift due to page view scroll.
173     * @param pos an array of two integers in which to hold the coordinates
174     */
175    private static void computeLocationRelativeToParent(View v, View parent, int[] pos) {
176        pos[0] = pos[1] = 0;
177        computeLocationRelativeToParentHelper(v, parent, pos);
178
179        // If a view is scaled, its position will also shift accordingly. For optimization, only
180        // consider this for the last node.
181        pos[0] += (1 - v.getScaleX()) * v.getWidth() / 2;
182        pos[1] += (1 - v.getScaleY()) * v.getHeight() / 2;
183    }
184
185    private static void computeLocationRelativeToParentHelper(View child,
186            View commonParent, int[] shift) {
187        View parent = (View) child.getParent();
188        shift[0] += child.getLeft();
189        shift[1] += child.getTop();
190        if (parent instanceof PagedView) {
191            PagedView page = (PagedView) parent;
192            shift[0] -= page.getScrollForPage(page.indexOfChild(child));
193        }
194
195        if (parent != commonParent) {
196            computeLocationRelativeToParentHelper(parent, commonParent, shift);
197        }
198    }
199
200    @Thunk static final class ViewAnimState {
201        float x, y, scaleX, scaleY;
202    }
203}
204