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