| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.dialog; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorInflater; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.ValueAnimator; |
| import android.app.ActivityManager; |
| import android.app.Dialog; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.SharedPreferences; |
| import android.content.res.Resources; |
| import android.media.tv.TvContentRating; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.preference.PreferenceManager; |
| import android.text.TextUtils; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewGroup.LayoutParams; |
| import android.widget.FrameLayout; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import com.android.tv.R; |
| import com.android.tv.TvApplication; |
| import com.android.tv.common.SoftPreconditions; |
| import com.android.tv.util.TvSettings; |
| |
| public class PinDialogFragment extends SafeDismissDialogFragment { |
| private static final String TAG = "PinDialogFragment"; |
| private static final boolean DEBUG = true; |
| |
| /** |
| * PIN code dialog for unlock channel |
| */ |
| public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0; |
| |
| /** |
| * PIN code dialog for unlock content. |
| * Only difference between {@code PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title. |
| */ |
| public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1; |
| |
| /** |
| * PIN code dialog for change parental control settings |
| */ |
| public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2; |
| |
| /** |
| * PIN code dialog for set new PIN |
| */ |
| public static final int PIN_DIALOG_TYPE_NEW_PIN = 3; |
| |
| // PIN code dialog for checking old PIN. This is internal only. |
| private static final int PIN_DIALOG_TYPE_OLD_PIN = 4; |
| |
| /** |
| * PIN code dialog for unlocking DVR playback |
| */ |
| public static final int PIN_DIALOG_TYPE_UNLOCK_DVR = 5; |
| |
| private static final int MAX_WRONG_PIN_COUNT = 5; |
| private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute |
| |
| private static final String INITIAL_TEXT = "—"; |
| private static final String TRACKER_LABEL = "Pin dialog"; |
| private static final String ARGS_TYPE = "args_type"; |
| private static final String ARGS_RATING = "args_rating"; |
| |
| public static final String DIALOG_TAG = PinDialogFragment.class.getName(); |
| |
| private static final int NUMBER_PICKERS_RES_ID[] = { |
| R.id.first, R.id.second, R.id.third, R.id.fourth }; |
| |
| private int mType; |
| private int mRequestType; |
| private boolean mPinChecked; |
| private boolean mDismissSilently; |
| |
| private TextView mWrongPinView; |
| private View mEnterPinView; |
| private TextView mTitleView; |
| private PinNumberPicker[] mPickers; |
| private SharedPreferences mSharedPreferences; |
| private String mPrevPin; |
| private String mPin; |
| private String mRatingString; |
| private int mWrongPinCount; |
| private long mDisablePinUntil; |
| private final Handler mHandler = new Handler(); |
| |
| public static PinDialogFragment create(int type) { |
| return create(type, null); |
| } |
| |
| public static PinDialogFragment create(int type, String rating) { |
| PinDialogFragment fragment = new PinDialogFragment(); |
| Bundle args = new Bundle(); |
| args.putInt(ARGS_TYPE, type); |
| args.putString(ARGS_RATING, rating); |
| fragment.setArguments(args); |
| return fragment; |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| mRequestType = getArguments().getInt(ARGS_TYPE, PIN_DIALOG_TYPE_ENTER_PIN); |
| mType = mRequestType; |
| mRatingString = getArguments().getString(ARGS_RATING); |
| setStyle(STYLE_NO_TITLE, 0); |
| mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); |
| mDisablePinUntil = TvSettings.getDisablePinUntil(getActivity()); |
| if (ActivityManager.isUserAMonkey()) { |
| // Skip PIN dialog half the time for monkeys |
| if (Math.random() < 0.5) { |
| exit(true); |
| } |
| } |
| mPinChecked = false; |
| } |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| Dialog dlg = super.onCreateDialog(savedInstanceState); |
| dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation; |
| PinNumberPicker.loadResources(dlg.getContext()); |
| return dlg; |
| } |
| |
| @Override |
| public String getTrackerLabel() { |
| return TRACKER_LABEL; |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| // Dialog size is determined by its windows size, not inflated view size. |
| // So apply view size to window after the DialogFragment.onStart() where dialog is shown. |
| Dialog dlg = getDialog(); |
| if (dlg != null) { |
| dlg.getWindow().setLayout( |
| getResources().getDimensionPixelSize(R.dimen.pin_dialog_width), |
| LayoutParams.WRAP_CONTENT); |
| } |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| final View v = inflater.inflate(R.layout.pin_dialog, container, false); |
| |
| mWrongPinView = (TextView) v.findViewById(R.id.wrong_pin); |
| mEnterPinView = v.findViewById(R.id.enter_pin); |
| mTitleView = (TextView) mEnterPinView.findViewById(R.id.title); |
| if (TextUtils.isEmpty(getPin())) { |
| // If PIN isn't set, user should set a PIN. |
| // Successfully setting a new set is considered as entering correct PIN. |
| mType = PIN_DIALOG_TYPE_NEW_PIN; |
| } |
| switch (mType) { |
| case PIN_DIALOG_TYPE_UNLOCK_CHANNEL: |
| mTitleView.setText(R.string.pin_enter_unlock_channel); |
| break; |
| case PIN_DIALOG_TYPE_UNLOCK_PROGRAM: |
| mTitleView.setText(R.string.pin_enter_unlock_program); |
| break; |
| case PIN_DIALOG_TYPE_UNLOCK_DVR: |
| TvContentRating tvContentRating = |
| TvContentRating.unflattenFromString(mRatingString); |
| if (TvContentRating.UNRATED.equals(tvContentRating)) { |
| mTitleView.setText(getString(R.string.pin_enter_unlock_dvr_unrated)); |
| } else { |
| mTitleView.setText( |
| getString( |
| R.string.pin_enter_unlock_dvr, |
| TvApplication.getSingletons(getContext()) |
| .getTvInputManagerHelper() |
| .getContentRatingsManager() |
| .getDisplayNameForRating(tvContentRating))); |
| } |
| break; |
| case PIN_DIALOG_TYPE_ENTER_PIN: |
| mTitleView.setText(R.string.pin_enter_pin); |
| break; |
| case PIN_DIALOG_TYPE_NEW_PIN: |
| if (TextUtils.isEmpty(getPin())) { |
| mTitleView.setText(R.string.pin_enter_create_pin); |
| } else { |
| mTitleView.setText(R.string.pin_enter_old_pin); |
| mType = PIN_DIALOG_TYPE_OLD_PIN; |
| } |
| } |
| |
| mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length]; |
| for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) { |
| mPickers[i] = (PinNumberPicker) v.findViewById(NUMBER_PICKERS_RES_ID[i]); |
| mPickers[i].setValueRangeAndResetText(0, 9); |
| mPickers[i].setPinDialogFragment(this); |
| mPickers[i].updateFocus(false); |
| } |
| for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) { |
| mPickers[i].setNextNumberPicker(mPickers[i + 1]); |
| } |
| |
| if (mType != PIN_DIALOG_TYPE_NEW_PIN) { |
| updateWrongPin(); |
| } |
| return v; |
| } |
| |
| private final Runnable mUpdateEnterPinRunnable = new Runnable() { |
| @Override |
| public void run() { |
| updateWrongPin(); |
| } |
| }; |
| |
| private void updateWrongPin() { |
| if (getActivity() == null) { |
| // The activity is already detached. No need to update. |
| mHandler.removeCallbacks(null); |
| return; |
| } |
| |
| int remainingSeconds = (int) ((mDisablePinUntil - System.currentTimeMillis()) / 1000); |
| boolean enabled = remainingSeconds < 1; |
| if (enabled) { |
| mWrongPinView.setVisibility(View.INVISIBLE); |
| mEnterPinView.setVisibility(View.VISIBLE); |
| mWrongPinCount = 0; |
| } else { |
| mEnterPinView.setVisibility(View.INVISIBLE); |
| mWrongPinView.setVisibility(View.VISIBLE); |
| mWrongPinView.setText(getResources().getQuantityString(R.plurals.pin_enter_countdown, |
| remainingSeconds, remainingSeconds)); |
| mHandler.postDelayed(mUpdateEnterPinRunnable, 1000); |
| } |
| } |
| |
| private void exit(boolean pinChecked) { |
| mPinChecked = pinChecked; |
| dismiss(); |
| } |
| |
| /** Dismisses the pin dialog without calling activity listener. */ |
| public void dismissSilently() { |
| mDismissSilently = true; |
| dismiss(); |
| } |
| |
| @Override |
| public void onDismiss(DialogInterface dialog) { |
| super.onDismiss(dialog); |
| if (DEBUG) Log.d(TAG, "onDismiss: mPinChecked=" + mPinChecked); |
| SoftPreconditions.checkState(getActivity() instanceof OnPinCheckedListener); |
| if (!mDismissSilently && getActivity() instanceof OnPinCheckedListener) { |
| ((OnPinCheckedListener) getActivity()).onPinChecked( |
| mPinChecked, mRequestType, mRatingString); |
| } |
| mDismissSilently = false; |
| } |
| |
| private void handleWrongPin() { |
| if (++mWrongPinCount >= MAX_WRONG_PIN_COUNT) { |
| mDisablePinUntil = System.currentTimeMillis() + DISABLE_PIN_DURATION_MILLIS; |
| TvSettings.setDisablePinUntil(getActivity(), mDisablePinUntil); |
| updateWrongPin(); |
| } else { |
| showToast(R.string.pin_toast_wrong); |
| } |
| } |
| |
| private void showToast(int resId) { |
| Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show(); |
| } |
| |
| private void done(String pin) { |
| if (DEBUG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin + " stored=" + getPin()); |
| switch (mType) { |
| case PIN_DIALOG_TYPE_UNLOCK_CHANNEL: |
| case PIN_DIALOG_TYPE_UNLOCK_PROGRAM: |
| case PIN_DIALOG_TYPE_UNLOCK_DVR: |
| case PIN_DIALOG_TYPE_ENTER_PIN: |
| if (TextUtils.isEmpty(getPin()) || pin.equals(getPin())) { |
| exit(true); |
| } else { |
| resetPinInput(); |
| handleWrongPin(); |
| } |
| break; |
| case PIN_DIALOG_TYPE_NEW_PIN: |
| resetPinInput(); |
| if (mPrevPin == null) { |
| mPrevPin = pin; |
| mTitleView.setText(R.string.pin_enter_again); |
| } else { |
| if (pin.equals(mPrevPin)) { |
| setPin(pin); |
| exit(true); |
| } else { |
| if (TextUtils.isEmpty(getPin())) { |
| mTitleView.setText(R.string.pin_enter_create_pin); |
| } else { |
| mTitleView.setText(R.string.pin_enter_new_pin); |
| } |
| mPrevPin = null; |
| showToast(R.string.pin_toast_not_match); |
| } |
| } |
| break; |
| case PIN_DIALOG_TYPE_OLD_PIN: |
| // Call resetPinInput() here because we'll get additional PIN input |
| // regardless of the result. |
| resetPinInput(); |
| if (pin.equals(getPin())) { |
| mType = PIN_DIALOG_TYPE_NEW_PIN; |
| mTitleView.setText(R.string.pin_enter_new_pin); |
| } else { |
| handleWrongPin(); |
| } |
| break; |
| } |
| } |
| |
| public int getType() { |
| return mType; |
| } |
| |
| private void setPin(String pin) { |
| if (DEBUG) Log.d(TAG, "setPin: " + pin); |
| mPin = pin; |
| mSharedPreferences.edit().putString(TvSettings.PREF_PIN, pin).apply(); |
| } |
| |
| private String getPin() { |
| if (mPin == null) { |
| mPin = mSharedPreferences.getString(TvSettings.PREF_PIN, ""); |
| } |
| return mPin; |
| } |
| |
| private String getPinInput() { |
| String result = ""; |
| try { |
| for (PinNumberPicker pnp : mPickers) { |
| pnp.updateText(); |
| result += pnp.getValue(); |
| } |
| } catch (IllegalStateException e) { |
| result = ""; |
| } |
| return result; |
| } |
| |
| private void resetPinInput() { |
| for (PinNumberPicker pnp : mPickers) { |
| pnp.setValueRangeAndResetText(0, 9); |
| } |
| mPickers[0].requestFocus(); |
| } |
| |
| public static class PinNumberPicker extends FrameLayout { |
| private static final int NUMBER_VIEWS_RES_ID[] = { |
| R.id.previous2_number, |
| R.id.previous_number, |
| R.id.current_number, |
| R.id.next_number, |
| R.id.next2_number }; |
| private static final int CURRENT_NUMBER_VIEW_INDEX = 2; |
| private static final int NOT_INITIALIZED = Integer.MIN_VALUE; |
| |
| private static Animator sFocusedNumberEnterAnimator; |
| private static Animator sFocusedNumberExitAnimator; |
| private static Animator sAdjacentNumberEnterAnimator; |
| private static Animator sAdjacentNumberExitAnimator; |
| |
| private static float sAlphaForFocusedNumber; |
| private static float sAlphaForAdjacentNumber; |
| |
| private int mMinValue; |
| private int mMaxValue; |
| private int mCurrentValue; |
| // a value for setting mCurrentValue at the end of scroll animation. |
| private int mNextValue; |
| private final int mNumberViewHeight; |
| private PinDialogFragment mDialog; |
| private PinNumberPicker mNextNumberPicker; |
| private boolean mCancelAnimation; |
| |
| private final View mNumberViewHolder; |
| // When the PinNumberPicker has focus, mBackgroundView will show the focused background. |
| // Also, this view is used for handling the text change animation of the current number |
| // view which is required when the current number view text is changing from INITIAL_TEXT |
| // to "0". |
| private final TextView mBackgroundView; |
| private final TextView[] mNumberViews; |
| private final AnimatorSet mFocusGainAnimator; |
| private final AnimatorSet mFocusLossAnimator; |
| private final AnimatorSet mScrollAnimatorSet; |
| |
| public PinNumberPicker(Context context) { |
| this(context, null); |
| } |
| |
| public PinNumberPicker(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| View view = inflate(context, R.layout.pin_number_picker, this); |
| mNumberViewHolder = view.findViewById(R.id.number_view_holder); |
| mBackgroundView = (TextView) view.findViewById(R.id.focused_background); |
| mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length]; |
| for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { |
| mNumberViews[i] = (TextView) view.findViewById(NUMBER_VIEWS_RES_ID[i]); |
| } |
| Resources resources = context.getResources(); |
| mNumberViewHeight = resources.getDimensionPixelSize( |
| R.dimen.pin_number_picker_text_view_height); |
| |
| mNumberViewHolder.setOnFocusChangeListener(new OnFocusChangeListener() { |
| @Override |
| public void onFocusChange(View v, boolean hasFocus) { |
| updateFocus(true); |
| } |
| }); |
| |
| mNumberViewHolder.setOnKeyListener(new OnKeyListener() { |
| @Override |
| public boolean onKey(View v, int keyCode, KeyEvent event) { |
| if (event.getAction() == KeyEvent.ACTION_DOWN) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_DPAD_UP: |
| case KeyEvent.KEYCODE_DPAD_DOWN: { |
| if (mCancelAnimation) { |
| mScrollAnimatorSet.end(); |
| } |
| if (!mScrollAnimatorSet.isRunning()) { |
| mCancelAnimation = false; |
| if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { |
| mNextValue = adjustValueInValidRange(mCurrentValue + 1); |
| startScrollAnimation(true); |
| } else { |
| mNextValue = adjustValueInValidRange(mCurrentValue - 1); |
| startScrollAnimation(false); |
| } |
| } |
| return true; |
| } |
| } |
| } else if (event.getAction() == KeyEvent.ACTION_UP) { |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_DPAD_UP: |
| case KeyEvent.KEYCODE_DPAD_DOWN: { |
| mCancelAnimation = true; |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| }); |
| mNumberViewHolder.setScrollY(mNumberViewHeight); |
| |
| mFocusGainAnimator = new AnimatorSet(); |
| mFocusGainAnimator.playTogether( |
| ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], |
| "alpha", 0f, sAlphaForAdjacentNumber), |
| ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX], |
| "alpha", sAlphaForFocusedNumber, 0f), |
| ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], |
| "alpha", 0f, sAlphaForAdjacentNumber), |
| ObjectAnimator.ofFloat(mBackgroundView, "alpha", 0f, 1f)); |
| mFocusGainAnimator.setDuration(context.getResources().getInteger( |
| android.R.integer.config_shortAnimTime)); |
| mFocusGainAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText(mBackgroundView.getText()); |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(sAlphaForFocusedNumber); |
| mBackgroundView.setText(""); |
| } |
| }); |
| |
| mFocusLossAnimator = new AnimatorSet(); |
| mFocusLossAnimator.playTogether( |
| ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1], |
| "alpha", sAlphaForAdjacentNumber, 0f), |
| ObjectAnimator.ofFloat(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1], |
| "alpha", sAlphaForAdjacentNumber, 0f), |
| ObjectAnimator.ofFloat(mBackgroundView, "alpha", 1f, 0f)); |
| mFocusLossAnimator.setDuration(context.getResources().getInteger( |
| android.R.integer.config_shortAnimTime)); |
| |
| mScrollAnimatorSet = new AnimatorSet(); |
| mScrollAnimatorSet.setDuration(context.getResources().getInteger( |
| R.integer.pin_number_scroll_duration)); |
| mScrollAnimatorSet.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| // Set mCurrent value when scroll animation is finished. |
| mCurrentValue = mNextValue; |
| updateText(); |
| mNumberViewHolder.setScrollY(mNumberViewHeight); |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber); |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setAlpha(sAlphaForFocusedNumber); |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber); |
| } |
| }); |
| } |
| |
| static void loadResources(Context context) { |
| if (sFocusedNumberEnterAnimator == null) { |
| TypedValue outValue = new TypedValue(); |
| context.getResources().getValue( |
| R.dimen.pin_alpha_for_focused_number, outValue, true); |
| sAlphaForFocusedNumber = outValue.getFloat(); |
| context.getResources().getValue( |
| R.dimen.pin_alpha_for_adjacent_number, outValue, true); |
| sAlphaForAdjacentNumber = outValue.getFloat(); |
| |
| sFocusedNumberEnterAnimator = AnimatorInflater.loadAnimator(context, |
| R.animator.pin_focused_number_enter); |
| sFocusedNumberExitAnimator = AnimatorInflater.loadAnimator(context, |
| R.animator.pin_focused_number_exit); |
| sAdjacentNumberEnterAnimator = AnimatorInflater.loadAnimator(context, |
| R.animator.pin_adjacent_number_enter); |
| sAdjacentNumberExitAnimator = AnimatorInflater.loadAnimator(context, |
| R.animator.pin_adjacent_number_exit); |
| } |
| } |
| |
| @Override |
| public boolean dispatchKeyEvent(KeyEvent event) { |
| if (event.getAction() == KeyEvent.ACTION_UP) { |
| int keyCode = event.getKeyCode(); |
| if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { |
| mNextValue = adjustValueInValidRange(keyCode - KeyEvent.KEYCODE_0); |
| updateFocus(false); |
| } else if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER |
| || keyCode == KeyEvent.KEYCODE_ENTER) { |
| if (mNextNumberPicker == null) { |
| String pin = mDialog.getPinInput(); |
| if (!TextUtils.isEmpty(pin)) { |
| mDialog.done(pin); |
| } |
| } else { |
| mNextNumberPicker.requestFocus(); |
| } |
| return true; |
| } |
| } |
| return super.dispatchKeyEvent(event); |
| } |
| |
| void startScrollAnimation(boolean scrollUp) { |
| mFocusGainAnimator.end(); |
| mFocusLossAnimator.end(); |
| final ValueAnimator scrollAnimator = ValueAnimator.ofInt( |
| 0, scrollUp ? mNumberViewHeight : -mNumberViewHeight); |
| scrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| int value = (Integer) animation.getAnimatedValue(); |
| mNumberViewHolder.setScrollY(value + mNumberViewHeight); |
| } |
| }); |
| scrollAnimator.setDuration( |
| getResources().getInteger(R.integer.pin_number_scroll_duration)); |
| |
| if (scrollUp) { |
| sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]); |
| sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]); |
| sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]); |
| sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 2]); |
| } else { |
| sAdjacentNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 2]); |
| sFocusedNumberEnterAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1]); |
| sFocusedNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX]); |
| sAdjacentNumberExitAnimator.setTarget(mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1]); |
| } |
| |
| mScrollAnimatorSet.playTogether(scrollAnimator, |
| sAdjacentNumberExitAnimator, sFocusedNumberExitAnimator, |
| sFocusedNumberEnterAnimator, sAdjacentNumberEnterAnimator); |
| mScrollAnimatorSet.start(); |
| } |
| |
| void setValueRangeAndResetText(int min, int max) { |
| if (min > max) { |
| throw new IllegalArgumentException( |
| "The min value should be greater than or equal to the max value"); |
| } else if (min == NOT_INITIALIZED) { |
| throw new IllegalArgumentException( |
| "The min value should be greater than Integer.MIN_VALUE."); |
| } |
| mMinValue = min; |
| mMaxValue = max; |
| mNextValue = mCurrentValue = NOT_INITIALIZED; |
| for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { |
| mNumberViews[i].setText(i == CURRENT_NUMBER_VIEW_INDEX ? INITIAL_TEXT : ""); |
| } |
| mBackgroundView.setText(INITIAL_TEXT); |
| } |
| |
| void setPinDialogFragment(PinDialogFragment dlg) { |
| mDialog = dlg; |
| } |
| |
| void setNextNumberPicker(PinNumberPicker picker) { |
| mNextNumberPicker = picker; |
| } |
| |
| int getValue() { |
| if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) { |
| throw new IllegalStateException("Value is not set"); |
| } |
| return mCurrentValue; |
| } |
| |
| void updateFocus(boolean withAnimation) { |
| mScrollAnimatorSet.end(); |
| mFocusGainAnimator.end(); |
| mFocusLossAnimator.end(); |
| updateText(); |
| if (mNumberViewHolder.isFocused()) { |
| if (withAnimation) { |
| mBackgroundView.setText(String.valueOf(mCurrentValue)); |
| mFocusGainAnimator.start(); |
| } else { |
| mBackgroundView.setAlpha(1f); |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(sAlphaForAdjacentNumber); |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(sAlphaForAdjacentNumber); |
| } |
| } else { |
| if (withAnimation) { |
| mFocusLossAnimator.start(); |
| } else { |
| mBackgroundView.setAlpha(0f); |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX - 1].setAlpha(0f); |
| mNumberViews[CURRENT_NUMBER_VIEW_INDEX + 1].setAlpha(0f); |
| } |
| mNumberViewHolder.setScrollY(mNumberViewHeight); |
| } |
| } |
| |
| private void updateText() { |
| boolean wasNotInitialized = false; |
| if (mNumberViewHolder.isFocused() && mCurrentValue == NOT_INITIALIZED) { |
| mNextValue = mCurrentValue = mMinValue; |
| wasNotInitialized = true; |
| } |
| if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) { |
| for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) { |
| if (wasNotInitialized && i == CURRENT_NUMBER_VIEW_INDEX) { |
| // In order to show the text change animation, keep the text of |
| // mNumberViews[CURRENT_NUMBER_VIEW_INDEX]. |
| } else { |
| mNumberViews[i].setText(String.valueOf(adjustValueInValidRange( |
| mCurrentValue - CURRENT_NUMBER_VIEW_INDEX + i))); |
| } |
| } |
| } |
| } |
| |
| private int adjustValueInValidRange(int value) { |
| int interval = mMaxValue - mMinValue + 1; |
| if (value < mMinValue - interval || value > mMaxValue + interval) { |
| throw new IllegalArgumentException("The value( " + value |
| + ") is too small or too big to adjust"); |
| } |
| return (value < mMinValue) ? value + interval |
| : (value > mMaxValue) ? value - interval : value; |
| } |
| } |
| |
| /** |
| * A listener to the result of {@link PinDialogFragment}. Any activity requiring pin code |
| * checking should implement this listener to receive the result. |
| */ |
| public interface OnPinCheckedListener { |
| /** |
| * Called when {@link PinDialogFragment} is dismissed. |
| * |
| * @param checked {@code true} if the pin code entered is checked to be correct, |
| * otherwise {@code false}. |
| * @param type The dialog type regarding to what pin entering is for. |
| * @param rating The target rating to unblock for. |
| */ |
| void onPinChecked(boolean checked, int type, String rating); |
| } |
| } |