1/*
2 * Copyright (C) 2009 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.pinyin;
18
19import com.android.inputmethod.pinyin.PinyinIME.DecodingInfo;
20
21import android.content.Context;
22import android.util.AttributeSet;
23import android.view.GestureDetector;
24import android.view.MotionEvent;
25import android.view.View;
26import android.view.View.OnTouchListener;
27import android.view.animation.AlphaAnimation;
28import android.view.animation.Animation;
29import android.view.animation.AnimationSet;
30import android.view.animation.TranslateAnimation;
31import android.view.animation.Animation.AnimationListener;
32import android.widget.ImageButton;
33import android.widget.RelativeLayout;
34import android.widget.ViewFlipper;
35
36interface ArrowUpdater {
37    void updateArrowStatus();
38}
39
40
41/**
42 * Container used to host the two candidate views. When user drags on candidate
43 * view, animation is used to dismiss the current candidate view and show a new
44 * one. These two candidate views and their parent are hosted by this container.
45 * <p>
46 * Besides the candidate views, there are two arrow views to show the page
47 * forward/backward arrows.
48 * </p>
49 */
50public class CandidatesContainer extends RelativeLayout implements
51        OnTouchListener, AnimationListener, ArrowUpdater {
52    /**
53     * Alpha value to show an enabled arrow.
54     */
55    private static int ARROW_ALPHA_ENABLED = 0xff;
56
57    /**
58     * Alpha value to show an disabled arrow.
59     */
60    private static int ARROW_ALPHA_DISABLED = 0x40;
61
62    /**
63     * Animation time to show a new candidate view and dismiss the old one.
64     */
65    private static int ANIMATION_TIME = 200;
66
67    /**
68     * Listener used to notify IME that user clicks a candidate, or navigate
69     * between them.
70     */
71    private CandidateViewListener mCvListener;
72
73    /**
74     * The left arrow button used to show previous page.
75     */
76    private ImageButton mLeftArrowBtn;
77
78    /**
79     * The right arrow button used to show next page.
80     */
81    private ImageButton mRightArrowBtn;
82
83    /**
84     * Decoding result to show.
85     */
86    private DecodingInfo mDecInfo;
87
88    /**
89     * The animation view used to show candidates. It contains two views.
90     * Normally, the candidates are shown one of them. When user navigates to
91     * another page, animation effect will be performed.
92     */
93    private ViewFlipper mFlipper;
94
95    /**
96     * The x offset of the flipper in this container.
97     */
98    private int xOffsetForFlipper;
99
100    /**
101     * Animation used by the incoming view when the user navigates to a left
102     * page.
103     */
104    private Animation mInAnimPushLeft;
105
106    /**
107     * Animation used by the incoming view when the user navigates to a right
108     * page.
109     */
110    private Animation mInAnimPushRight;
111
112    /**
113     * Animation used by the incoming view when the user navigates to a page
114     * above. If the page navigation is triggered by DOWN key, this animation is
115     * used.
116     */
117    private Animation mInAnimPushUp;
118
119    /**
120     * Animation used by the incoming view when the user navigates to a page
121     * below. If the page navigation is triggered by UP key, this animation is
122     * used.
123     */
124    private Animation mInAnimPushDown;
125
126    /**
127     * Animation used by the outgoing view when the user navigates to a left
128     * page.
129     */
130    private Animation mOutAnimPushLeft;
131
132    /**
133     * Animation used by the outgoing view when the user navigates to a right
134     * page.
135     */
136    private Animation mOutAnimPushRight;
137
138    /**
139     * Animation used by the outgoing view when the user navigates to a page
140     * above. If the page navigation is triggered by DOWN key, this animation is
141     * used.
142     */
143    private Animation mOutAnimPushUp;
144
145    /**
146     * Animation used by the incoming view when the user navigates to a page
147     * below. If the page navigation is triggered by UP key, this animation is
148     * used.
149     */
150    private Animation mOutAnimPushDown;
151
152    /**
153     * Animation object which is used for the incoming view currently.
154     */
155    private Animation mInAnimInUse;
156
157    /**
158     * Animation object which is used for the outgoing view currently.
159     */
160    private Animation mOutAnimInUse;
161
162    /**
163     * Current page number in display.
164     */
165    private int mCurrentPage = -1;
166
167    public CandidatesContainer(Context context, AttributeSet attrs) {
168        super(context, attrs);
169    }
170
171    public void initialize(CandidateViewListener cvListener,
172            BalloonHint balloonHint, GestureDetector gestureDetector) {
173        mCvListener = cvListener;
174
175        mLeftArrowBtn = (ImageButton) findViewById(R.id.arrow_left_btn);
176        mRightArrowBtn = (ImageButton) findViewById(R.id.arrow_right_btn);
177        mLeftArrowBtn.setOnTouchListener(this);
178        mRightArrowBtn.setOnTouchListener(this);
179
180        mFlipper = (ViewFlipper) findViewById(R.id.candidate_flipper);
181        mFlipper.setMeasureAllChildren(true);
182
183        invalidate();
184        requestLayout();
185
186        for (int i = 0; i < mFlipper.getChildCount(); i++) {
187            CandidateView cv = (CandidateView) mFlipper.getChildAt(i);
188            cv.initialize(this, balloonHint, gestureDetector, mCvListener);
189        }
190    }
191
192    public void showCandidates(PinyinIME.DecodingInfo decInfo,
193            boolean enableActiveHighlight) {
194        if (null == decInfo) return;
195        mDecInfo = decInfo;
196        mCurrentPage = 0;
197
198        if (decInfo.isCandidatesListEmpty()) {
199            showArrow(mLeftArrowBtn, false);
200            showArrow(mRightArrowBtn, false);
201        } else {
202            showArrow(mLeftArrowBtn, true);
203            showArrow(mRightArrowBtn, true);
204        }
205
206        for (int i = 0; i < mFlipper.getChildCount(); i++) {
207            CandidateView cv = (CandidateView) mFlipper.getChildAt(i);
208            cv.setDecodingInfo(mDecInfo);
209        }
210        stopAnimation();
211
212        CandidateView cv = (CandidateView) mFlipper.getCurrentView();
213        cv.showPage(mCurrentPage, 0, enableActiveHighlight);
214
215        updateArrowStatus();
216        invalidate();
217    }
218
219    public int getCurrentPage() {
220        return mCurrentPage;
221    }
222
223    public void enableActiveHighlight(boolean enableActiveHighlight) {
224        CandidateView cv = (CandidateView) mFlipper.getCurrentView();
225        cv.enableActiveHighlight(enableActiveHighlight);
226        invalidate();
227    }
228
229    @Override
230    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
231        Environment env = Environment.getInstance();
232        int measuredWidth = env.getScreenWidth();
233        int measuredHeight = getPaddingTop();
234        measuredHeight += env.getHeightForCandidates();
235        widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth,
236                MeasureSpec.EXACTLY);
237        heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight,
238                MeasureSpec.EXACTLY);
239        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
240
241        if (null != mLeftArrowBtn) {
242            xOffsetForFlipper = mLeftArrowBtn.getMeasuredWidth();
243        }
244    }
245
246    public boolean activeCurseBackward() {
247        if (mFlipper.isFlipping() || null == mDecInfo) {
248            return false;
249        }
250
251        CandidateView cv = (CandidateView) mFlipper.getCurrentView();
252
253        if (cv.activeCurseBackward()) {
254            cv.invalidate();
255            return true;
256        } else {
257            return pageBackward(true, true);
258        }
259    }
260
261    public boolean activeCurseForward() {
262        if (mFlipper.isFlipping() || null == mDecInfo) {
263            return false;
264        }
265
266        CandidateView cv = (CandidateView) mFlipper.getCurrentView();
267
268        if (cv.activeCursorForward()) {
269            cv.invalidate();
270            return true;
271        } else {
272            return pageForward(true, true);
273        }
274    }
275
276    public boolean pageBackward(boolean animLeftRight,
277            boolean enableActiveHighlight) {
278        if (null == mDecInfo) return false;
279
280        if (mFlipper.isFlipping() || 0 == mCurrentPage) return false;
281
282        int child = mFlipper.getDisplayedChild();
283        int childNext = (child + 1) % 2;
284        CandidateView cv = (CandidateView) mFlipper.getChildAt(child);
285        CandidateView cvNext = (CandidateView) mFlipper.getChildAt(childNext);
286
287        mCurrentPage--;
288        int activeCandInPage = cv.getActiveCandiatePosInPage();
289        if (animLeftRight)
290            activeCandInPage = mDecInfo.mPageStart.elementAt(mCurrentPage + 1)
291                    - mDecInfo.mPageStart.elementAt(mCurrentPage) - 1;
292
293        cvNext.showPage(mCurrentPage, activeCandInPage, enableActiveHighlight);
294        loadAnimation(animLeftRight, false);
295        startAnimation();
296
297        updateArrowStatus();
298        return true;
299    }
300
301    public boolean pageForward(boolean animLeftRight,
302            boolean enableActiveHighlight) {
303        if (null == mDecInfo) return false;
304
305        if (mFlipper.isFlipping() || !mDecInfo.preparePage(mCurrentPage + 1)) {
306            return false;
307        }
308
309        int child = mFlipper.getDisplayedChild();
310        int childNext = (child + 1) % 2;
311        CandidateView cv = (CandidateView) mFlipper.getChildAt(child);
312        int activeCandInPage = cv.getActiveCandiatePosInPage();
313        cv.enableActiveHighlight(enableActiveHighlight);
314
315        CandidateView cvNext = (CandidateView) mFlipper.getChildAt(childNext);
316        mCurrentPage++;
317        if (animLeftRight) activeCandInPage = 0;
318
319        cvNext.showPage(mCurrentPage, activeCandInPage, enableActiveHighlight);
320        loadAnimation(animLeftRight, true);
321        startAnimation();
322
323        updateArrowStatus();
324        return true;
325    }
326
327    public int getActiveCandiatePos() {
328        if (null == mDecInfo) return -1;
329        CandidateView cv = (CandidateView) mFlipper.getCurrentView();
330        return cv.getActiveCandiatePosGlobal();
331    }
332
333    public void updateArrowStatus() {
334        if (mCurrentPage < 0) return;
335        boolean forwardEnabled = mDecInfo.pageForwardable(mCurrentPage);
336        boolean backwardEnabled = mDecInfo.pageBackwardable(mCurrentPage);
337
338        if (backwardEnabled) {
339            enableArrow(mLeftArrowBtn, true);
340        } else {
341            enableArrow(mLeftArrowBtn, false);
342        }
343        if (forwardEnabled) {
344            enableArrow(mRightArrowBtn, true);
345        } else {
346            enableArrow(mRightArrowBtn, false);
347        }
348    }
349
350    private void enableArrow(ImageButton arrowBtn, boolean enabled) {
351        arrowBtn.setEnabled(enabled);
352        if (enabled)
353            arrowBtn.setAlpha(ARROW_ALPHA_ENABLED);
354        else
355            arrowBtn.setAlpha(ARROW_ALPHA_DISABLED);
356    }
357
358    private void showArrow(ImageButton arrowBtn, boolean show) {
359        if (show)
360            arrowBtn.setVisibility(View.VISIBLE);
361        else
362            arrowBtn.setVisibility(View.INVISIBLE);
363    }
364
365    public boolean onTouch(View v, MotionEvent event) {
366        if (event.getAction() == MotionEvent.ACTION_DOWN) {
367            if (v == mLeftArrowBtn) {
368                mCvListener.onToRightGesture();
369            } else if (v == mRightArrowBtn) {
370                mCvListener.onToLeftGesture();
371            }
372        } else if (event.getAction() == MotionEvent.ACTION_UP) {
373            CandidateView cv = (CandidateView) mFlipper.getCurrentView();
374            cv.enableActiveHighlight(true);
375        }
376
377        return false;
378    }
379
380    // The reason why we handle candiate view's touch events here is because
381    // that the view under the focused view may get touch events instead of the
382    // focused one.
383    @Override
384    public boolean onTouchEvent(MotionEvent event) {
385        event.offsetLocation(-xOffsetForFlipper, 0);
386        CandidateView cv = (CandidateView) mFlipper.getCurrentView();
387        cv.onTouchEventReal(event);
388        return true;
389    }
390
391    public void loadAnimation(boolean animLeftRight, boolean forward) {
392        if (animLeftRight) {
393            if (forward) {
394                if (null == mInAnimPushLeft) {
395                    mInAnimPushLeft = createAnimation(1.0f, 0, 0, 0, 0, 1.0f,
396                            ANIMATION_TIME);
397                    mOutAnimPushLeft = createAnimation(0, -1.0f, 0, 0, 1.0f, 0,
398                            ANIMATION_TIME);
399                }
400                mInAnimInUse = mInAnimPushLeft;
401                mOutAnimInUse = mOutAnimPushLeft;
402            } else {
403                if (null == mInAnimPushRight) {
404                    mInAnimPushRight = createAnimation(-1.0f, 0, 0, 0, 0, 1.0f,
405                            ANIMATION_TIME);
406                    mOutAnimPushRight = createAnimation(0, 1.0f, 0, 0, 1.0f, 0,
407                            ANIMATION_TIME);
408                }
409                mInAnimInUse = mInAnimPushRight;
410                mOutAnimInUse = mOutAnimPushRight;
411            }
412        } else {
413            if (forward) {
414                if (null == mInAnimPushUp) {
415                    mInAnimPushUp = createAnimation(0, 0, 1.0f, 0, 0, 1.0f,
416                            ANIMATION_TIME);
417                    mOutAnimPushUp = createAnimation(0, 0, 0, -1.0f, 1.0f, 0,
418                            ANIMATION_TIME);
419                }
420                mInAnimInUse = mInAnimPushUp;
421                mOutAnimInUse = mOutAnimPushUp;
422            } else {
423                if (null == mInAnimPushDown) {
424                    mInAnimPushDown = createAnimation(0, 0, -1.0f, 0, 0, 1.0f,
425                            ANIMATION_TIME);
426                    mOutAnimPushDown = createAnimation(0, 0, 0, 1.0f, 1.0f, 0,
427                            ANIMATION_TIME);
428                }
429                mInAnimInUse = mInAnimPushDown;
430                mOutAnimInUse = mOutAnimPushDown;
431            }
432        }
433
434        mInAnimInUse.setAnimationListener(this);
435
436        mFlipper.setInAnimation(mInAnimInUse);
437        mFlipper.setOutAnimation(mOutAnimInUse);
438    }
439
440    private Animation createAnimation(float xFrom, float xTo, float yFrom,
441            float yTo, float alphaFrom, float alphaTo, long duration) {
442        AnimationSet animSet = new AnimationSet(getContext(), null);
443        Animation trans = new TranslateAnimation(Animation.RELATIVE_TO_SELF,
444                xFrom, Animation.RELATIVE_TO_SELF, xTo,
445                Animation.RELATIVE_TO_SELF, yFrom, Animation.RELATIVE_TO_SELF,
446                yTo);
447        Animation alpha = new AlphaAnimation(alphaFrom, alphaTo);
448        animSet.addAnimation(trans);
449        animSet.addAnimation(alpha);
450        animSet.setDuration(duration);
451        return animSet;
452    }
453
454    private void startAnimation() {
455        mFlipper.showNext();
456    }
457
458    private void stopAnimation() {
459        mFlipper.stopFlipping();
460    }
461
462    public void onAnimationEnd(Animation animation) {
463        if (!mLeftArrowBtn.isPressed() && !mRightArrowBtn.isPressed()) {
464            CandidateView cv = (CandidateView) mFlipper.getCurrentView();
465            cv.enableActiveHighlight(true);
466        }
467    }
468
469    public void onAnimationRepeat(Animation animation) {
470    }
471
472    public void onAnimationStart(Animation animation) {
473    }
474}
475