KeyPreviewChoreographer.java revision 84405d2a6815a99992849e821e073835f2d892e3
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 com.android.inputmethod.keyboard.internal;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.Context;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.animation.AccelerateInterpolator;
27import android.view.animation.DecelerateInterpolator;
28
29import com.android.inputmethod.keyboard.Key;
30import com.android.inputmethod.latin.utils.CoordinateUtils;
31import com.android.inputmethod.latin.utils.ViewLayoutUtils;
32
33import java.util.ArrayDeque;
34import java.util.HashMap;
35import java.util.HashSet;
36
37/**
38 * This class controls pop up key previews. This class decides:
39 * - what kind of key previews should be shown.
40 * - where key previews should be placed.
41 * - how key previews should be shown and dismissed.
42 */
43public final class KeyPreviewChoreographer {
44    // Free {@link KeyPreviewView} pool that can be used for key preview.
45    private final ArrayDeque<KeyPreviewView> mFreeKeyPreviewViews = new ArrayDeque<>();
46    // Map from {@link Key} to {@link KeyPreviewView} that is currently being displayed as key
47    // preview.
48    private final HashMap<Key,KeyPreviewView> mShowingKeyPreviewViews = new HashMap<>();
49
50    private final KeyPreviewDrawParams mParams;
51
52    public KeyPreviewChoreographer(final KeyPreviewDrawParams params) {
53        mParams = params;
54    }
55
56    public KeyPreviewView getKeyPreviewView(final Key key, final ViewGroup placerView) {
57        KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.remove(key);
58        if (keyPreviewView != null) {
59            return keyPreviewView;
60        }
61        keyPreviewView = mFreeKeyPreviewViews.poll();
62        if (keyPreviewView != null) {
63            return keyPreviewView;
64        }
65        final Context context = placerView.getContext();
66        keyPreviewView = new KeyPreviewView(context, null /* attrs */);
67        keyPreviewView.setBackgroundResource(mParams.mPreviewBackgroundResId);
68        placerView.addView(keyPreviewView, ViewLayoutUtils.newLayoutParam(placerView, 0, 0));
69        return keyPreviewView;
70    }
71
72    public boolean isShowingKeyPreview(final Key key) {
73        return mShowingKeyPreviewViews.containsKey(key);
74    }
75
76    public void dismissAllKeyPreviews() {
77        for (final Key key : new HashSet<>(mShowingKeyPreviewViews.keySet())) {
78            dismissKeyPreview(key, false /* withAnimation */);
79        }
80    }
81
82    public void dismissKeyPreview(final Key key, final boolean withAnimation) {
83        if (key == null) {
84            return;
85        }
86        final KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.get(key);
87        if (keyPreviewView == null) {
88            return;
89        }
90        final Object tag = keyPreviewView.getTag();
91        if (withAnimation) {
92            if (tag instanceof KeyPreviewAnimations) {
93                final KeyPreviewAnimations animation = (KeyPreviewAnimations)tag;
94                animation.startDismiss();
95                return;
96            }
97        }
98        // Dismiss preview without animation.
99        mShowingKeyPreviewViews.remove(key);
100        if (tag instanceof Animator) {
101            ((Animator)tag).cancel();
102        }
103        keyPreviewView.setTag(null);
104        keyPreviewView.setVisibility(View.INVISIBLE);
105        mFreeKeyPreviewViews.add(keyPreviewView);
106    }
107
108    public void placeAndShowKeyPreview(final Key key, final KeyboardIconsSet iconsSet,
109            final KeyDrawParams drawParams, final int keyboardViewWidth, final int[] keyboardOrigin,
110            final ViewGroup placerView, final boolean withAnimation) {
111        final KeyPreviewView keyPreviewView = getKeyPreviewView(key, placerView);
112        placeKeyPreview(
113                key, keyPreviewView, iconsSet, drawParams, keyboardViewWidth, keyboardOrigin);
114        showKeyPreview(key, keyPreviewView, withAnimation);
115    }
116
117    private void placeKeyPreview(final Key key, final KeyPreviewView keyPreviewView,
118            final KeyboardIconsSet iconsSet, final KeyDrawParams drawParams,
119            final int keyboardViewWidth, final int[] originCoords) {
120        keyPreviewView.setPreviewVisual(key, iconsSet, drawParams);
121        keyPreviewView.measure(
122                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
123        mParams.setGeometry(keyPreviewView);
124        final int previewWidth = keyPreviewView.getMeasuredWidth();
125        final int previewHeight = mParams.mPreviewHeight;
126        final int keyDrawWidth = key.getDrawWidth();
127        // The key preview is horizontally aligned with the center of the visible part of the
128        // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
129        // the left/right background is used if such background is specified.
130        final int keyPreviewPosition;
131        int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2
132                + CoordinateUtils.x(originCoords);
133        if (previewX < 0) {
134            previewX = 0;
135            keyPreviewPosition = KeyPreviewView.POSITION_LEFT;
136        } else if (previewX > keyboardViewWidth - previewWidth) {
137            previewX = keyboardViewWidth - previewWidth;
138            keyPreviewPosition = KeyPreviewView.POSITION_RIGHT;
139        } else {
140            keyPreviewPosition = KeyPreviewView.POSITION_MIDDLE;
141        }
142        final boolean hasMoreKeys = (key.getMoreKeys() != null);
143        keyPreviewView.setPreviewBackground(hasMoreKeys, keyPreviewPosition);
144        // The key preview is placed vertically above the top edge of the parent key with an
145        // arbitrary offset.
146        final int previewY = key.getY() - previewHeight + mParams.mPreviewOffset
147                + CoordinateUtils.y(originCoords);
148
149        ViewLayoutUtils.placeViewAt(
150                keyPreviewView, previewX, previewY, previewWidth, previewHeight);
151        keyPreviewView.setPivotX(previewWidth / 2.0f);
152        keyPreviewView.setPivotY(previewHeight);
153    }
154
155    private void showKeyPreview(final Key key, final KeyPreviewView keyPreviewView,
156            final boolean withAnimation) {
157        if (!withAnimation) {
158            keyPreviewView.setVisibility(View.VISIBLE);
159            mShowingKeyPreviewViews.put(key, keyPreviewView);
160            return;
161        }
162
163        // Show preview with animation.
164        final Animator showUpAnimation = createShowUpAniation(key, keyPreviewView);
165        final Animator dismissAnimation = createDismissAnimation(key, keyPreviewView);
166        final KeyPreviewAnimations animation = new KeyPreviewAnimations(
167                showUpAnimation, dismissAnimation);
168        keyPreviewView.setTag(animation);
169        animation.startShowUp();
170    }
171
172    private static final float KEY_PREVIEW_SHOW_UP_END_SCALE = 1.0f;
173    private static final AccelerateInterpolator ACCELERATE_INTERPOLATOR =
174            new AccelerateInterpolator();
175    private static final DecelerateInterpolator DECELERATE_INTERPOLATOR =
176            new DecelerateInterpolator();
177
178    private Animator createShowUpAniation(final Key key, final KeyPreviewView keyPreviewView) {
179        // TODO: Optimization for no scale animation and no duration.
180        final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat(
181                keyPreviewView, View.SCALE_X, mParams.getShowUpStartScale(),
182                KEY_PREVIEW_SHOW_UP_END_SCALE);
183        final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat(
184                keyPreviewView, View.SCALE_Y, mParams.getShowUpStartScale(),
185                KEY_PREVIEW_SHOW_UP_END_SCALE);
186        final AnimatorSet showUpAnimation = new AnimatorSet();
187        showUpAnimation.play(scaleXAnimation).with(scaleYAnimation);
188        showUpAnimation.setDuration(mParams.getShowUpDuration());
189        showUpAnimation.setInterpolator(DECELERATE_INTERPOLATOR);
190        showUpAnimation.addListener(new AnimatorListenerAdapter() {
191            @Override
192            public void onAnimationStart(final Animator animation) {
193                showKeyPreview(key, keyPreviewView, false /* withAnimation */);
194            }
195        });
196        return showUpAnimation;
197    }
198
199    private Animator createDismissAnimation(final Key key, final KeyPreviewView keyPreviewView) {
200        // TODO: Optimization for no scale animation and no duration.
201        final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat(
202                keyPreviewView, View.SCALE_X, mParams.getDismissEndScale());
203        final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat(
204                keyPreviewView, View.SCALE_Y, mParams.getDismissEndScale());
205        final AnimatorSet dismissAnimation = new AnimatorSet();
206        dismissAnimation.play(scaleXAnimation).with(scaleYAnimation);
207        final int dismissDuration = Math.min(
208                mParams.getDismissDuration(), mParams.getLingerTimeout());
209        dismissAnimation.setDuration(dismissDuration);
210        dismissAnimation.setInterpolator(ACCELERATE_INTERPOLATOR);
211        dismissAnimation.addListener(new AnimatorListenerAdapter() {
212            @Override
213            public void onAnimationEnd(final Animator animation) {
214                dismissKeyPreview(key, false /* withAnimation */);
215            }
216        });
217        return dismissAnimation;
218    }
219
220    private static class KeyPreviewAnimations extends AnimatorListenerAdapter {
221        private final Animator mShowUpAnimation;
222        private final Animator mDismissAnimation;
223
224        public KeyPreviewAnimations(final Animator showUpAnimation,
225                final Animator dismissAnimation) {
226            mShowUpAnimation = showUpAnimation;
227            mDismissAnimation = dismissAnimation;
228        }
229
230        public void startShowUp() {
231            mShowUpAnimation.start();
232        }
233
234        public void startDismiss() {
235            if (mShowUpAnimation.isRunning()) {
236                mShowUpAnimation.addListener(this);
237                return;
238            }
239            mDismissAnimation.start();
240        }
241
242        @Override
243        public void onAnimationEnd(final Animator animation) {
244            mDismissAnimation.start();
245        }
246    }
247}
248