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 17 package com.android.inputmethod.pinyin; 18 19 import com.android.inputmethod.pinyin.PinyinIME.DecodingInfo; 20 21 import android.content.Context; 22 import android.util.AttributeSet; 23 import android.view.GestureDetector; 24 import android.view.MotionEvent; 25 import android.view.View; 26 import android.view.View.OnTouchListener; 27 import android.view.animation.AlphaAnimation; 28 import android.view.animation.Animation; 29 import android.view.animation.AnimationSet; 30 import android.view.animation.TranslateAnimation; 31 import android.view.animation.Animation.AnimationListener; 32 import android.widget.ImageButton; 33 import android.widget.RelativeLayout; 34 import android.widget.ViewFlipper; 35 36 interface 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 */ 50 public 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