/**
   * Initialize the Layout with starting values.
   *
   * @param context
   * @param initialHoursOfDay
   * @param initialMinutes
   * @param is24HourMode
   */
  public void initialize(
      Context context,
      int initialHoursOfDay,
      int initialMinutes,
      boolean is24HourMode,
      boolean vibrate) {
    if (mTimeInitialized) {
      Log.e(TAG, "Time has already been initialized.");
      return;
    }
    mIs24HourMode = is24HourMode;
    mHideAmPm = Utils.isTouchExplorationEnabled(mAccessibilityManager) ? true : mIs24HourMode;

    mVibrate = vibrate;

    // Initialize the circle and AM/PM circles if applicable.
    mCircleView.initialize(context, mHideAmPm);
    mCircleView.invalidate();
    if (!mHideAmPm) {
      mAmPmCirclesView.initialize(context, initialHoursOfDay < 12 ? AM : PM);
      mAmPmCirclesView.invalidate();
    }

    // Initialize the hours and minutes numbers.
    Resources res = context.getResources();
    int[] hours = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    int[] hours_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
    int[] minutes = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
    String[] hoursTexts = new String[12];
    String[] innerHoursTexts = new String[12];
    String[] minutesTexts = new String[12];
    for (int i = 0; i < 12; i++) {
      hoursTexts[i] =
          is24HourMode ? String.format("%02d", hours_24[i]) : String.format("%d", hours[i]);
      innerHoursTexts[i] = String.format("%d", hours[i]);
      minutesTexts[i] = String.format("%02d", minutes[i]);
    }
    mHourRadialTextsView.initialize(
        res, hoursTexts, (is24HourMode ? innerHoursTexts : null), mHideAmPm, true);
    mHourRadialTextsView.invalidate();
    mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false);
    mMinuteRadialTextsView.invalidate();

    // Initialize the currently-selected hour and minute.
    setValueForItem(HOUR_INDEX, initialHoursOfDay);
    setValueForItem(MINUTE_INDEX, initialMinutes);
    int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
    mHourRadialSelectorView.initialize(
        context, mHideAmPm, is24HourMode, true, hourDegrees, isHourInnerCircle(initialHoursOfDay));
    int minuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
    mMinuteRadialSelectorView.initialize(context, mHideAmPm, false, false, minuteDegrees, false);

    mTimeInitialized = true;
  }
  /**
   * Initialize this selector with the state of the picker.
   *
   * @param context Current context.
   * @param is24HourMode Whether the selector is in 24-hour mode, which will tell us whether the
   *     circle's center is moved up slightly to make room for the AM/PM circles.
   * @param hasInnerCircle Whether we have both an inner and an outer circle of numbers that may be
   *     selected. Should be true for 24-hour mode in the hours circle.
   * @param disappearsOut Whether the numbers' animation will have them disappearing out or
   *     disappearing in.
   * @param selectionDegrees The initial degrees to be selected.
   * @param isInnerCircle Whether the initial selection is in the inner or outer circle. Will be
   *     ignored when hasInnerCircle is false.
   */
  public void initialize(
      Context context,
      boolean is24HourMode,
      boolean hasInnerCircle,
      boolean disappearsOut,
      int selectionDegrees,
      boolean isInnerCircle) {
    if (mIsInitialized) {
      Log.e(TAG, "This RadialSelectorView may only be initialized once.");
      return;
    }

    Resources res = context.getResources();

    int blue =
        Utils.resolveColor(getContext(), R.attr.theme_color, res.getColor(R.color.comm_blue));
    mPaint.setColor(blue);
    mPaint.setAntiAlias(true);

    // Calculate values for the circle radius size.
    mIs24HourMode = is24HourMode;
    if (is24HourMode) {
      mCircleRadiusMultiplier =
          Float.parseFloat(res.getString(R.string.circle_radius_multiplier_24HourMode));
    } else {
      mCircleRadiusMultiplier = Float.parseFloat(res.getString(R.string.circle_radius_multiplier));
      mAmPmCircleRadiusMultiplier =
          Float.parseFloat(res.getString(R.string.ampm_circle_radius_multiplier));
    }

    // Calculate values for the radius size(s) of the numbers circle(s).
    mHasInnerCircle = hasInnerCircle;
    if (hasInnerCircle) {
      mInnerNumbersRadiusMultiplier =
          Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_inner));
      mOuterNumbersRadiusMultiplier =
          Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_outer));
    } else {
      mNumbersRadiusMultiplier =
          Float.parseFloat(res.getString(R.string.numbers_radius_multiplier_normal));
    }
    mSelectionRadiusMultiplier =
        Float.parseFloat(res.getString(R.string.selection_radius_multiplier));

    // Calculate values for the transition mid-way states.
    mAnimationRadiusMultiplier = 1;
    mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut ? -1 : 1));
    mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut ? 1 : -1));
    mInvalidateUpdateListener = new InvalidateUpdateListener();

    setSelection(selectionDegrees, isInnerCircle, false);
    mIsInitialized = true;
  }
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    final float eventX = event.getX();
    final float eventY = event.getY();
    int degrees;
    int value;
    final Boolean[] isInnerCircle = new Boolean[1];
    isInnerCircle[0] = false;

    long millis = SystemClock.uptimeMillis();

    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        if (!mInputEnabled) {
          return true;
        }

        mDownX = eventX;
        mDownY = eventY;

        mLastValueSelected = -1;
        mDoingMove = false;
        mDoingTouch = true;
        // If we're showing the AM/PM, check to see if the user is touching it.
        if (!mHideAmPm) {
          mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
        } else {
          mIsTouchingAmOrPm = -1;
        }
        if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
          // If the touch is on AM or PM, set it as "touched" after the TAP_TIMEOUT
          // in case the user moves their finger quickly.
          tryVibrate();
          mDownDegrees = -1;
          mHandler.postDelayed(
              new Runnable() {
                @Override
                public void run() {
                  mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm);
                  mAmPmCirclesView.invalidate();
                }
              },
              TAP_TIMEOUT);
        } else {
          // If we're in accessibility mode, force the touch to be legal. Otherwise,
          // it will only register within the given touch target zone.
          boolean forceLegal = Utils.isTouchExplorationEnabled(mAccessibilityManager);
          // Calculate the degrees that is currently being touched.
          mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle);
          if (mDownDegrees != -1) {
            // If it's a legal touch, set that number as "selected" after the
            // TAP_TIMEOUT in case the user moves their finger quickly.
            tryVibrate();
            mHandler.postDelayed(
                new Runnable() {
                  @Override
                  public void run() {
                    mDoingMove = true;
                    int value = reselectSelector(mDownDegrees, isInnerCircle[0], false, true);
                    mLastValueSelected = value;
                    mListener.onValueSelected(getCurrentItemShowing(), value, false);
                  }
                },
                TAP_TIMEOUT);
          }
        }
        return true;
      case MotionEvent.ACTION_MOVE:
        if (!mInputEnabled) {
          // We shouldn't be in this state, because input is disabled.
          Log.e(TAG, "Input was disabled, but received ACTION_MOVE.");
          return true;
        }

        float dY = Math.abs(eventY - mDownY);
        float dX = Math.abs(eventX - mDownX);

        if (!mDoingMove && dX <= TOUCH_SLOP && dY <= TOUCH_SLOP) {
          // Hasn't registered down yet, just slight, accidental movement of finger.
          break;
        }

        // If we're in the middle of touching down on AM or PM, check if we still are.
        // If so, no-op. If not, remove its pressed state. Either way, no need to check
        // for touches on the other circle.
        if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
          mHandler.removeCallbacksAndMessages(null);
          int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
          if (isTouchingAmOrPm != mIsTouchingAmOrPm) {
            mAmPmCirclesView.setAmOrPmPressed(-1);
            mAmPmCirclesView.invalidate();
            mIsTouchingAmOrPm = -1;
          }
          break;
        }

        if (mDownDegrees == -1) {
          // Original down was illegal, so no movement will register.
          break;
        }

        // We're doing a move along the circle, so move the selection as appropriate.
        mDoingMove = true;
        mHandler.removeCallbacksAndMessages(null);
        degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle);
        if (degrees != -1) {
          value = reselectSelector(degrees, isInnerCircle[0], false, true);
          if (value != mLastValueSelected) {
            tryVibrate();
            mLastValueSelected = value;
            mListener.onValueSelected(getCurrentItemShowing(), value, false);
          }
        }
        return true;
      case MotionEvent.ACTION_UP:
        if (!mInputEnabled) {
          // If our touch input was disabled, tell the listener to re-enable us.
          Log.d(TAG, "Input was disabled, but received ACTION_UP.");
          mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false);
          return true;
        }

        mHandler.removeCallbacksAndMessages(null);
        mDoingTouch = false;

        // If we're touching AM or PM, set it as selected, and tell the listener.
        if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
          int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
          mAmPmCirclesView.setAmOrPmPressed(-1);
          mAmPmCirclesView.invalidate();

          if (isTouchingAmOrPm == mIsTouchingAmOrPm) {
            mAmPmCirclesView.setAmOrPm(isTouchingAmOrPm);
            if (getIsCurrentlyAmOrPm() != isTouchingAmOrPm) {
              mListener.onValueSelected(AMPM_INDEX, mIsTouchingAmOrPm, false);
              setValueForItem(AMPM_INDEX, isTouchingAmOrPm);
            }
          }
          mIsTouchingAmOrPm = -1;
          break;
        }

        // If we have a legal degrees selected, set the value and tell the listener.
        if (mDownDegrees != -1) {
          degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle);
          if (degrees != -1) {
            value = reselectSelector(degrees, isInnerCircle[0], !mDoingMove, false);
            if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) {
              int amOrPm = getIsCurrentlyAmOrPm();
              if (amOrPm == AM && value == 12) {
                value = 0;
              } else if (amOrPm == PM && value != 12) {
                value += 12;
              }
            }
            setValueForItem(getCurrentItemShowing(), value);
            mListener.onValueSelected(getCurrentItemShowing(), value, true);
          }
        }
        mDoingMove = false;
        return true;
      default:
        break;
    }
    return false;
  }