|  | /* | 
|  | * Copyright (C) 2013 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 android.widget; | 
|  |  | 
|  | import android.annotation.IntDef; | 
|  | import android.annotation.Nullable; | 
|  | import android.annotation.TestApi; | 
|  | import android.content.Context; | 
|  | import android.content.res.ColorStateList; | 
|  | import android.content.res.Resources; | 
|  | import android.content.res.TypedArray; | 
|  | import android.icu.text.DecimalFormatSymbols; | 
|  | import android.os.Parcelable; | 
|  | import android.text.SpannableStringBuilder; | 
|  | import android.text.TextUtils; | 
|  | import android.text.format.DateFormat; | 
|  | import android.text.format.DateUtils; | 
|  | import android.text.style.TtsSpan; | 
|  | import android.util.AttributeSet; | 
|  | import android.util.StateSet; | 
|  | import android.view.HapticFeedbackConstants; | 
|  | import android.view.LayoutInflater; | 
|  | import android.view.MotionEvent; | 
|  | import android.view.View; | 
|  | import android.view.View.AccessibilityDelegate; | 
|  | import android.view.View.MeasureSpec; | 
|  | import android.view.ViewGroup; | 
|  | import android.view.accessibility.AccessibilityEvent; | 
|  | import android.view.accessibility.AccessibilityNodeInfo; | 
|  | import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; | 
|  | import android.view.inputmethod.InputMethodManager; | 
|  | import android.widget.RadialTimePickerView.OnValueSelectedListener; | 
|  | import android.widget.TextInputTimePickerView.OnValueTypedListener; | 
|  |  | 
|  | import com.android.internal.R; | 
|  | import com.android.internal.widget.NumericTextView; | 
|  | import com.android.internal.widget.NumericTextView.OnValueChangedListener; | 
|  |  | 
|  |  | 
|  | import java.lang.annotation.Retention; | 
|  | import java.lang.annotation.RetentionPolicy; | 
|  | import java.util.Calendar; | 
|  |  | 
|  | /** | 
|  | * A delegate implementing the radial clock-based TimePicker. | 
|  | */ | 
|  | class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { | 
|  | /** | 
|  | * Delay in milliseconds before valid but potentially incomplete, for | 
|  | * example "1" but not "12", keyboard edits are propagated from the | 
|  | * hour / minute fields to the radial picker. | 
|  | */ | 
|  | private static final long DELAY_COMMIT_MILLIS = 2000; | 
|  |  | 
|  | @IntDef({FROM_EXTERNAL_API, FROM_RADIAL_PICKER, FROM_INPUT_PICKER}) | 
|  | @Retention(RetentionPolicy.SOURCE) | 
|  | private @interface ChangeSource {} | 
|  | private static final int FROM_EXTERNAL_API = 0; | 
|  | private static final int FROM_RADIAL_PICKER = 1; | 
|  | private static final int FROM_INPUT_PICKER = 2; | 
|  |  | 
|  | // Index used by RadialPickerLayout | 
|  | private static final int HOUR_INDEX = RadialTimePickerView.HOURS; | 
|  | private static final int MINUTE_INDEX = RadialTimePickerView.MINUTES; | 
|  |  | 
|  | private static final int[] ATTRS_TEXT_COLOR = new int[] {R.attr.textColor}; | 
|  | private static final int[] ATTRS_DISABLED_ALPHA = new int[] {R.attr.disabledAlpha}; | 
|  |  | 
|  | private static final int AM = 0; | 
|  | private static final int PM = 1; | 
|  |  | 
|  | private static final int HOURS_IN_HALF_DAY = 12; | 
|  |  | 
|  | private final NumericTextView mHourView; | 
|  | private final NumericTextView mMinuteView; | 
|  | private final View mAmPmLayout; | 
|  | private final RadioButton mAmLabel; | 
|  | private final RadioButton mPmLabel; | 
|  | private final RadialTimePickerView mRadialTimePickerView; | 
|  | private final TextView mSeparatorView; | 
|  |  | 
|  | private boolean mRadialPickerModeEnabled = true; | 
|  | private final ImageButton mRadialTimePickerModeButton; | 
|  | private final String mRadialTimePickerModeEnabledDescription; | 
|  | private final String mTextInputPickerModeEnabledDescription; | 
|  | private final View mRadialTimePickerHeader; | 
|  | private final View mTextInputPickerHeader; | 
|  |  | 
|  | private final TextInputTimePickerView mTextInputPickerView; | 
|  |  | 
|  | private final Calendar mTempCalendar; | 
|  |  | 
|  | // Accessibility strings. | 
|  | private final String mSelectHours; | 
|  | private final String mSelectMinutes; | 
|  |  | 
|  | private boolean mIsEnabled = true; | 
|  | private boolean mAllowAutoAdvance; | 
|  | private int mCurrentHour; | 
|  | private int mCurrentMinute; | 
|  | private boolean mIs24Hour; | 
|  |  | 
|  | // The portrait layout puts AM/PM at the right by default. | 
|  | private boolean mIsAmPmAtLeft = false; | 
|  | // The landscape layouts put AM/PM at the bottom by default. | 
|  | private boolean mIsAmPmAtTop = false; | 
|  |  | 
|  | // Localization data. | 
|  | private boolean mHourFormatShowLeadingZero; | 
|  | private boolean mHourFormatStartsAtZero; | 
|  |  | 
|  | // Most recent time announcement values for accessibility. | 
|  | private CharSequence mLastAnnouncedText; | 
|  | private boolean mLastAnnouncedIsHour; | 
|  |  | 
|  | public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs, | 
|  | int defStyleAttr, int defStyleRes) { | 
|  | super(delegator, context); | 
|  |  | 
|  | // process style attributes | 
|  | final TypedArray a = mContext.obtainStyledAttributes(attrs, | 
|  | R.styleable.TimePicker, defStyleAttr, defStyleRes); | 
|  | final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( | 
|  | Context.LAYOUT_INFLATER_SERVICE); | 
|  | final Resources res = mContext.getResources(); | 
|  |  | 
|  | mSelectHours = res.getString(R.string.select_hours); | 
|  | mSelectMinutes = res.getString(R.string.select_minutes); | 
|  |  | 
|  | final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout, | 
|  | R.layout.time_picker_material); | 
|  | final View mainView = inflater.inflate(layoutResourceId, delegator); | 
|  | mainView.setSaveFromParentEnabled(false); | 
|  | mRadialTimePickerHeader = mainView.findViewById(R.id.time_header); | 
|  | mRadialTimePickerHeader.setOnTouchListener(new NearestTouchDelegate()); | 
|  |  | 
|  | // Set up hour/minute labels. | 
|  | mHourView = (NumericTextView) mainView.findViewById(R.id.hours); | 
|  | mHourView.setOnClickListener(mClickListener); | 
|  | mHourView.setOnFocusChangeListener(mFocusListener); | 
|  | mHourView.setOnDigitEnteredListener(mDigitEnteredListener); | 
|  | mHourView.setAccessibilityDelegate( | 
|  | new ClickActionDelegate(context, R.string.select_hours)); | 
|  | mSeparatorView = (TextView) mainView.findViewById(R.id.separator); | 
|  | mMinuteView = (NumericTextView) mainView.findViewById(R.id.minutes); | 
|  | mMinuteView.setOnClickListener(mClickListener); | 
|  | mMinuteView.setOnFocusChangeListener(mFocusListener); | 
|  | mMinuteView.setOnDigitEnteredListener(mDigitEnteredListener); | 
|  | mMinuteView.setAccessibilityDelegate( | 
|  | new ClickActionDelegate(context, R.string.select_minutes)); | 
|  | mMinuteView.setRange(0, 59); | 
|  |  | 
|  | // Set up AM/PM labels. | 
|  | mAmPmLayout = mainView.findViewById(R.id.ampm_layout); | 
|  | mAmPmLayout.setOnTouchListener(new NearestTouchDelegate()); | 
|  |  | 
|  | final String[] amPmStrings = TimePicker.getAmPmStrings(context); | 
|  | mAmLabel = (RadioButton) mAmPmLayout.findViewById(R.id.am_label); | 
|  | mAmLabel.setText(obtainVerbatim(amPmStrings[0])); | 
|  | mAmLabel.setOnClickListener(mClickListener); | 
|  | ensureMinimumTextWidth(mAmLabel); | 
|  |  | 
|  | mPmLabel = (RadioButton) mAmPmLayout.findViewById(R.id.pm_label); | 
|  | mPmLabel.setText(obtainVerbatim(amPmStrings[1])); | 
|  | mPmLabel.setOnClickListener(mClickListener); | 
|  | ensureMinimumTextWidth(mPmLabel); | 
|  |  | 
|  | // For the sake of backwards compatibility, attempt to extract the text | 
|  | // color from the header time text appearance. If it's set, we'll let | 
|  | // that override the "real" header text color. | 
|  | ColorStateList headerTextColor = null; | 
|  |  | 
|  | @SuppressWarnings("deprecation") | 
|  | final int timeHeaderTextAppearance = a.getResourceId( | 
|  | R.styleable.TimePicker_headerTimeTextAppearance, 0); | 
|  | if (timeHeaderTextAppearance != 0) { | 
|  | final TypedArray textAppearance = mContext.obtainStyledAttributes(null, | 
|  | ATTRS_TEXT_COLOR, 0, timeHeaderTextAppearance); | 
|  | final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0); | 
|  | headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor); | 
|  | textAppearance.recycle(); | 
|  | } | 
|  |  | 
|  | if (headerTextColor == null) { | 
|  | headerTextColor = a.getColorStateList(R.styleable.TimePicker_headerTextColor); | 
|  | } | 
|  |  | 
|  | mTextInputPickerHeader = mainView.findViewById(R.id.input_header); | 
|  |  | 
|  | if (headerTextColor != null) { | 
|  | mHourView.setTextColor(headerTextColor); | 
|  | mSeparatorView.setTextColor(headerTextColor); | 
|  | mMinuteView.setTextColor(headerTextColor); | 
|  | mAmLabel.setTextColor(headerTextColor); | 
|  | mPmLabel.setTextColor(headerTextColor); | 
|  | } | 
|  |  | 
|  | // Set up header background, if available. | 
|  | if (a.hasValueOrEmpty(R.styleable.TimePicker_headerBackground)) { | 
|  | mRadialTimePickerHeader.setBackground(a.getDrawable( | 
|  | R.styleable.TimePicker_headerBackground)); | 
|  | mTextInputPickerHeader.setBackground(a.getDrawable( | 
|  | R.styleable.TimePicker_headerBackground)); | 
|  | } | 
|  |  | 
|  | a.recycle(); | 
|  |  | 
|  | mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById(R.id.radial_picker); | 
|  | mRadialTimePickerView.applyAttributes(attrs, defStyleAttr, defStyleRes); | 
|  | mRadialTimePickerView.setOnValueSelectedListener(mOnValueSelectedListener); | 
|  |  | 
|  | mTextInputPickerView = (TextInputTimePickerView) mainView.findViewById(R.id.input_mode); | 
|  | mTextInputPickerView.setListener(mOnValueTypedListener); | 
|  |  | 
|  | mRadialTimePickerModeButton = | 
|  | (ImageButton) mainView.findViewById(R.id.toggle_mode); | 
|  | mRadialTimePickerModeButton.setOnClickListener(new View.OnClickListener() { | 
|  | @Override | 
|  | public void onClick(View v) { | 
|  | toggleRadialPickerMode(); | 
|  | } | 
|  | }); | 
|  | mRadialTimePickerModeEnabledDescription = context.getResources().getString( | 
|  | R.string.time_picker_radial_mode_description); | 
|  | mTextInputPickerModeEnabledDescription = context.getResources().getString( | 
|  | R.string.time_picker_text_input_mode_description); | 
|  |  | 
|  | mAllowAutoAdvance = true; | 
|  |  | 
|  | updateHourFormat(); | 
|  |  | 
|  | // Initialize with current time. | 
|  | mTempCalendar = Calendar.getInstance(mLocale); | 
|  | final int currentHour = mTempCalendar.get(Calendar.HOUR_OF_DAY); | 
|  | final int currentMinute = mTempCalendar.get(Calendar.MINUTE); | 
|  | initialize(currentHour, currentMinute, mIs24Hour, HOUR_INDEX); | 
|  | } | 
|  |  | 
|  | private void toggleRadialPickerMode() { | 
|  | if (mRadialPickerModeEnabled) { | 
|  | mRadialTimePickerView.setVisibility(View.GONE); | 
|  | mRadialTimePickerHeader.setVisibility(View.GONE); | 
|  | mTextInputPickerHeader.setVisibility(View.VISIBLE); | 
|  | mTextInputPickerView.setVisibility(View.VISIBLE); | 
|  | mRadialTimePickerModeButton.setImageResource(R.drawable.btn_clock_material); | 
|  | mRadialTimePickerModeButton.setContentDescription( | 
|  | mRadialTimePickerModeEnabledDescription); | 
|  | mRadialPickerModeEnabled = false; | 
|  | } else { | 
|  | mRadialTimePickerView.setVisibility(View.VISIBLE); | 
|  | mRadialTimePickerHeader.setVisibility(View.VISIBLE); | 
|  | mTextInputPickerHeader.setVisibility(View.GONE); | 
|  | mTextInputPickerView.setVisibility(View.GONE); | 
|  | mRadialTimePickerModeButton.setImageResource(R.drawable.btn_keyboard_key_material); | 
|  | mRadialTimePickerModeButton.setContentDescription( | 
|  | mTextInputPickerModeEnabledDescription); | 
|  | updateTextInputPicker(); | 
|  | InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); | 
|  | if (imm != null) { | 
|  | imm.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); | 
|  | } | 
|  | mRadialPickerModeEnabled = true; | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean validateInput() { | 
|  | return mTextInputPickerView.validateInput(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Ensures that a TextView is wide enough to contain its text without | 
|  | * wrapping or clipping. Measures the specified view and sets the minimum | 
|  | * width to the view's desired width. | 
|  | * | 
|  | * @param v the text view to measure | 
|  | */ | 
|  | private static void ensureMinimumTextWidth(TextView v) { | 
|  | v.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); | 
|  |  | 
|  | // Set both the TextView and the View version of minimum | 
|  | // width because they are subtly different. | 
|  | final int minWidth = v.getMeasuredWidth(); | 
|  | v.setMinWidth(minWidth); | 
|  | v.setMinimumWidth(minWidth); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Updates hour formatting based on the current locale and 24-hour mode. | 
|  | * <p> | 
|  | * Determines how the hour should be formatted, sets member variables for | 
|  | * leading zero and starting hour, and sets the hour view's presentation. | 
|  | */ | 
|  | private void updateHourFormat() { | 
|  | final String bestDateTimePattern = DateFormat.getBestDateTimePattern( | 
|  | mLocale, mIs24Hour ? "Hm" : "hm"); | 
|  | final int lengthPattern = bestDateTimePattern.length(); | 
|  | boolean showLeadingZero = false; | 
|  | char hourFormat = '\0'; | 
|  |  | 
|  | for (int i = 0; i < lengthPattern; i++) { | 
|  | final char c = bestDateTimePattern.charAt(i); | 
|  | if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { | 
|  | hourFormat = c; | 
|  | if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { | 
|  | showLeadingZero = true; | 
|  | } | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | mHourFormatShowLeadingZero = showLeadingZero; | 
|  | mHourFormatStartsAtZero = hourFormat == 'K' || hourFormat == 'H'; | 
|  |  | 
|  | // Update hour text field. | 
|  | final int minHour = mHourFormatStartsAtZero ? 0 : 1; | 
|  | final int maxHour = (mIs24Hour ? 23 : 11) + minHour; | 
|  | mHourView.setRange(minHour, maxHour); | 
|  | mHourView.setShowLeadingZeroes(mHourFormatShowLeadingZero); | 
|  |  | 
|  | final String[] digits = DecimalFormatSymbols.getInstance(mLocale).getDigitStrings(); | 
|  | int maxCharLength = 0; | 
|  | for (int i = 0; i < 10; i++) { | 
|  | maxCharLength = Math.max(maxCharLength, digits[i].length()); | 
|  | } | 
|  | mTextInputPickerView.setHourFormat(maxCharLength * 2); | 
|  | } | 
|  |  | 
|  | static final CharSequence obtainVerbatim(String text) { | 
|  | return new SpannableStringBuilder().append(text, | 
|  | new TtsSpan.VerbatimBuilder(text).build(), 0); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * The legacy text color might have been poorly defined. Ensures that it | 
|  | * has an appropriate activated state, using the selected state if one | 
|  | * exists or modifying the default text color otherwise. | 
|  | * | 
|  | * @param color a legacy text color, or {@code null} | 
|  | * @return a color state list with an appropriate activated state, or | 
|  | *         {@code null} if a valid activated state could not be generated | 
|  | */ | 
|  | @Nullable | 
|  | private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) { | 
|  | if (color == null || color.hasState(R.attr.state_activated)) { | 
|  | return color; | 
|  | } | 
|  |  | 
|  | final int activatedColor; | 
|  | final int defaultColor; | 
|  | if (color.hasState(R.attr.state_selected)) { | 
|  | activatedColor = color.getColorForState(StateSet.get( | 
|  | StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0); | 
|  | defaultColor = color.getColorForState(StateSet.get( | 
|  | StateSet.VIEW_STATE_ENABLED), 0); | 
|  | } else { | 
|  | activatedColor = color.getDefaultColor(); | 
|  |  | 
|  | // Generate a non-activated color using the disabled alpha. | 
|  | final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA); | 
|  | final float disabledAlpha = ta.getFloat(0, 0.30f); | 
|  | defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha); | 
|  | } | 
|  |  | 
|  | if (activatedColor == 0 || defaultColor == 0) { | 
|  | // We somehow failed to obtain the colors. | 
|  | return null; | 
|  | } | 
|  |  | 
|  | final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}}; | 
|  | final int[] colors = new int[] { activatedColor, defaultColor }; | 
|  | return new ColorStateList(stateSet, colors); | 
|  | } | 
|  |  | 
|  | private int multiplyAlphaComponent(int color, float alphaMod) { | 
|  | final int srcRgb = color & 0xFFFFFF; | 
|  | final int srcAlpha = (color >> 24) & 0xFF; | 
|  | final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f); | 
|  | return srcRgb | (dstAlpha << 24); | 
|  | } | 
|  |  | 
|  | private static class ClickActionDelegate extends AccessibilityDelegate { | 
|  | private final AccessibilityAction mClickAction; | 
|  |  | 
|  | public ClickActionDelegate(Context context, int resId) { | 
|  | mClickAction = new AccessibilityAction( | 
|  | AccessibilityNodeInfo.ACTION_CLICK, context.getString(resId)); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { | 
|  | super.onInitializeAccessibilityNodeInfo(host, info); | 
|  |  | 
|  | info.addAction(mClickAction); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) { | 
|  | mCurrentHour = hourOfDay; | 
|  | mCurrentMinute = minute; | 
|  | mIs24Hour = is24HourView; | 
|  | updateUI(index); | 
|  | } | 
|  |  | 
|  | private void updateUI(int index) { | 
|  | updateHeaderAmPm(); | 
|  | updateHeaderHour(mCurrentHour, false); | 
|  | updateHeaderSeparator(); | 
|  | updateHeaderMinute(mCurrentMinute, false); | 
|  | updateRadialPicker(index); | 
|  | updateTextInputPicker(); | 
|  |  | 
|  | mDelegator.invalidate(); | 
|  | } | 
|  |  | 
|  | private void updateTextInputPicker() { | 
|  | mTextInputPickerView.updateTextInputValues(getLocalizedHour(mCurrentHour), mCurrentMinute, | 
|  | mCurrentHour < 12 ? AM : PM, mIs24Hour, mHourFormatStartsAtZero); | 
|  | } | 
|  |  | 
|  | private void updateRadialPicker(int index) { | 
|  | mRadialTimePickerView.initialize(mCurrentHour, mCurrentMinute, mIs24Hour); | 
|  | setCurrentItemShowing(index, false, true); | 
|  | } | 
|  |  | 
|  | private void updateHeaderAmPm() { | 
|  | if (mIs24Hour) { | 
|  | mAmPmLayout.setVisibility(View.GONE); | 
|  | } else { | 
|  | // Find the location of AM/PM based on locale information. | 
|  | final String dateTimePattern = DateFormat.getBestDateTimePattern(mLocale, "hm"); | 
|  | final boolean isAmPmAtStart = dateTimePattern.startsWith("a"); | 
|  | setAmPmStart(isAmPmAtStart); | 
|  | updateAmPmLabelStates(mCurrentHour < 12 ? AM : PM); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void setAmPmStart(boolean isAmPmAtStart) { | 
|  | final RelativeLayout.LayoutParams params = | 
|  | (RelativeLayout.LayoutParams) mAmPmLayout.getLayoutParams(); | 
|  | if (params.getRule(RelativeLayout.RIGHT_OF) != 0 | 
|  | || params.getRule(RelativeLayout.LEFT_OF) != 0) { | 
|  | final int margin = (int) (mContext.getResources().getDisplayMetrics().density * 8); | 
|  | // Horizontal mode, with AM/PM appearing to left/right of hours and minutes. | 
|  | final boolean isAmPmAtLeft; | 
|  | if (TextUtils.getLayoutDirectionFromLocale(mLocale) == View.LAYOUT_DIRECTION_LTR) { | 
|  | isAmPmAtLeft = isAmPmAtStart; | 
|  | } else { | 
|  | isAmPmAtLeft = !isAmPmAtStart; | 
|  | } | 
|  |  | 
|  | if (isAmPmAtLeft) { | 
|  | params.removeRule(RelativeLayout.RIGHT_OF); | 
|  | params.addRule(RelativeLayout.LEFT_OF, mHourView.getId()); | 
|  | } else { | 
|  | params.removeRule(RelativeLayout.LEFT_OF); | 
|  | params.addRule(RelativeLayout.RIGHT_OF, mMinuteView.getId()); | 
|  | } | 
|  |  | 
|  | if (isAmPmAtStart) { | 
|  | params.setMarginStart(0); | 
|  | params.setMarginEnd(margin); | 
|  | } else { | 
|  | params.setMarginStart(margin); | 
|  | params.setMarginEnd(0); | 
|  | } | 
|  | mIsAmPmAtLeft = isAmPmAtLeft; | 
|  | } else if (params.getRule(RelativeLayout.BELOW) != 0 | 
|  | || params.getRule(RelativeLayout.ABOVE) != 0) { | 
|  | // Vertical mode, with AM/PM appearing to top/bottom of hours and minutes. | 
|  | if (mIsAmPmAtTop == isAmPmAtStart) { | 
|  | // AM/PM is already at the correct location. No change needed. | 
|  | return; | 
|  | } | 
|  |  | 
|  | final int otherViewId; | 
|  | if (isAmPmAtStart) { | 
|  | otherViewId = params.getRule(RelativeLayout.BELOW); | 
|  | params.removeRule(RelativeLayout.BELOW); | 
|  | params.addRule(RelativeLayout.ABOVE, otherViewId); | 
|  | } else { | 
|  | otherViewId = params.getRule(RelativeLayout.ABOVE); | 
|  | params.removeRule(RelativeLayout.ABOVE); | 
|  | params.addRule(RelativeLayout.BELOW, otherViewId); | 
|  | } | 
|  |  | 
|  | // Switch the top and bottom paddings on the other view. | 
|  | final View otherView = mRadialTimePickerHeader.findViewById(otherViewId); | 
|  | final int top = otherView.getPaddingTop(); | 
|  | final int bottom = otherView.getPaddingBottom(); | 
|  | final int left = otherView.getPaddingLeft(); | 
|  | final int right = otherView.getPaddingRight(); | 
|  | otherView.setPadding(left, bottom, right, top); | 
|  |  | 
|  | mIsAmPmAtTop = isAmPmAtStart; | 
|  | } | 
|  |  | 
|  | mAmPmLayout.setLayoutParams(params); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setDate(int hour, int minute) { | 
|  | setHourInternal(hour, FROM_EXTERNAL_API, true, false); | 
|  | setMinuteInternal(minute, FROM_EXTERNAL_API, false); | 
|  |  | 
|  | onTimeChanged(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Set the current hour. | 
|  | */ | 
|  | @Override | 
|  | public void setHour(int hour) { | 
|  | setHourInternal(hour, FROM_EXTERNAL_API, true, true); | 
|  | } | 
|  |  | 
|  | private void setHourInternal(int hour, @ChangeSource int source, boolean announce, | 
|  | boolean notify) { | 
|  | if (mCurrentHour == hour) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | resetAutofilledValue(); | 
|  | mCurrentHour = hour; | 
|  | updateHeaderHour(hour, announce); | 
|  | updateHeaderAmPm(); | 
|  |  | 
|  | if (source != FROM_RADIAL_PICKER) { | 
|  | mRadialTimePickerView.setCurrentHour(hour); | 
|  | mRadialTimePickerView.setAmOrPm(hour < 12 ? AM : PM); | 
|  | } | 
|  | if (source != FROM_INPUT_PICKER) { | 
|  | updateTextInputPicker(); | 
|  | } | 
|  |  | 
|  | mDelegator.invalidate(); | 
|  | if (notify) { | 
|  | onTimeChanged(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return the current hour in the range (0-23) | 
|  | */ | 
|  | @Override | 
|  | public int getHour() { | 
|  | final int currentHour = mRadialTimePickerView.getCurrentHour(); | 
|  | if (mIs24Hour) { | 
|  | return currentHour; | 
|  | } | 
|  |  | 
|  | if (mRadialTimePickerView.getAmOrPm() == PM) { | 
|  | return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; | 
|  | } else { | 
|  | return currentHour % HOURS_IN_HALF_DAY; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Set the current minute (0-59). | 
|  | */ | 
|  | @Override | 
|  | public void setMinute(int minute) { | 
|  | setMinuteInternal(minute, FROM_EXTERNAL_API, true); | 
|  | } | 
|  |  | 
|  | private void setMinuteInternal(int minute, @ChangeSource int source, boolean notify) { | 
|  | if (mCurrentMinute == minute) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | resetAutofilledValue(); | 
|  | mCurrentMinute = minute; | 
|  | updateHeaderMinute(minute, true); | 
|  |  | 
|  | if (source != FROM_RADIAL_PICKER) { | 
|  | mRadialTimePickerView.setCurrentMinute(minute); | 
|  | } | 
|  | if (source != FROM_INPUT_PICKER) { | 
|  | updateTextInputPicker(); | 
|  | } | 
|  |  | 
|  | mDelegator.invalidate(); | 
|  | if (notify) { | 
|  | onTimeChanged(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return The current minute. | 
|  | */ | 
|  | @Override | 
|  | public int getMinute() { | 
|  | return mRadialTimePickerView.getCurrentMinute(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Sets whether time is displayed in 24-hour mode or 12-hour mode with | 
|  | * AM/PM indicators. | 
|  | * | 
|  | * @param is24Hour {@code true} to display time in 24-hour mode or | 
|  | *        {@code false} for 12-hour mode with AM/PM | 
|  | */ | 
|  | public void setIs24Hour(boolean is24Hour) { | 
|  | if (mIs24Hour != is24Hour) { | 
|  | mIs24Hour = is24Hour; | 
|  | mCurrentHour = getHour(); | 
|  |  | 
|  | updateHourFormat(); | 
|  | updateUI(mRadialTimePickerView.getCurrentItemShowing()); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return {@code true} if time is displayed in 24-hour mode, or | 
|  | *         {@code false} if time is displayed in 12-hour mode with AM/PM | 
|  | *         indicators | 
|  | */ | 
|  | @Override | 
|  | public boolean is24Hour() { | 
|  | return mIs24Hour; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void setEnabled(boolean enabled) { | 
|  | mHourView.setEnabled(enabled); | 
|  | mMinuteView.setEnabled(enabled); | 
|  | mAmLabel.setEnabled(enabled); | 
|  | mPmLabel.setEnabled(enabled); | 
|  | mRadialTimePickerView.setEnabled(enabled); | 
|  | mIsEnabled = enabled; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean isEnabled() { | 
|  | return mIsEnabled; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int getBaseline() { | 
|  | // does not support baseline alignment | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public Parcelable onSaveInstanceState(Parcelable superState) { | 
|  | return new SavedState(superState, getHour(), getMinute(), | 
|  | is24Hour(), getCurrentItemShowing()); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onRestoreInstanceState(Parcelable state) { | 
|  | if (state instanceof SavedState) { | 
|  | final SavedState ss = (SavedState) state; | 
|  | initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing()); | 
|  | mRadialTimePickerView.invalidate(); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { | 
|  | onPopulateAccessibilityEvent(event); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onPopulateAccessibilityEvent(AccessibilityEvent event) { | 
|  | int flags = DateUtils.FORMAT_SHOW_TIME; | 
|  | if (mIs24Hour) { | 
|  | flags |= DateUtils.FORMAT_24HOUR; | 
|  | } else { | 
|  | flags |= DateUtils.FORMAT_12HOUR; | 
|  | } | 
|  |  | 
|  | mTempCalendar.set(Calendar.HOUR_OF_DAY, getHour()); | 
|  | mTempCalendar.set(Calendar.MINUTE, getMinute()); | 
|  |  | 
|  | final String selectedTime = DateUtils.formatDateTime(mContext, | 
|  | mTempCalendar.getTimeInMillis(), flags); | 
|  | final String selectionMode = mRadialTimePickerView.getCurrentItemShowing() == HOUR_INDEX ? | 
|  | mSelectHours : mSelectMinutes; | 
|  | event.getText().add(selectedTime + " " + selectionMode); | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | @Override | 
|  | @TestApi | 
|  | public View getHourView() { | 
|  | return mHourView; | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | @Override | 
|  | @TestApi | 
|  | public View getMinuteView() { | 
|  | return mMinuteView; | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | @Override | 
|  | @TestApi | 
|  | public View getAmView() { | 
|  | return mAmLabel; | 
|  | } | 
|  |  | 
|  | /** @hide */ | 
|  | @Override | 
|  | @TestApi | 
|  | public View getPmView() { | 
|  | return mPmLabel; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return the index of the current item showing | 
|  | */ | 
|  | private int getCurrentItemShowing() { | 
|  | return mRadialTimePickerView.getCurrentItemShowing(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Propagate the time change | 
|  | */ | 
|  | private void onTimeChanged() { | 
|  | mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); | 
|  | if (mOnTimeChangedListener != null) { | 
|  | mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute()); | 
|  | } | 
|  | if (mAutoFillChangeListener != null) { | 
|  | mAutoFillChangeListener.onTimeChanged(mDelegator, getHour(), getMinute()); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void tryVibrate() { | 
|  | mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); | 
|  | } | 
|  |  | 
|  | private void updateAmPmLabelStates(int amOrPm) { | 
|  | final boolean isAm = amOrPm == AM; | 
|  | mAmLabel.setActivated(isAm); | 
|  | mAmLabel.setChecked(isAm); | 
|  |  | 
|  | final boolean isPm = amOrPm == PM; | 
|  | mPmLabel.setActivated(isPm); | 
|  | mPmLabel.setChecked(isPm); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Converts hour-of-day (0-23) time into a localized hour number. | 
|  | * <p> | 
|  | * The localized value may be in the range (0-23), (1-24), (0-11), or | 
|  | * (1-12) depending on the locale. This method does not handle leading | 
|  | * zeroes. | 
|  | * | 
|  | * @param hourOfDay the hour-of-day (0-23) | 
|  | * @return a localized hour number | 
|  | */ | 
|  | private int getLocalizedHour(int hourOfDay) { | 
|  | if (!mIs24Hour) { | 
|  | // Convert to hour-of-am-pm. | 
|  | hourOfDay %= 12; | 
|  | } | 
|  |  | 
|  | if (!mHourFormatStartsAtZero && hourOfDay == 0) { | 
|  | // Convert to clock-hour (either of-day or of-am-pm). | 
|  | hourOfDay = mIs24Hour ? 24 : 12; | 
|  | } | 
|  |  | 
|  | return hourOfDay; | 
|  | } | 
|  |  | 
|  | private void updateHeaderHour(int hourOfDay, boolean announce) { | 
|  | final int localizedHour = getLocalizedHour(hourOfDay); | 
|  | mHourView.setValue(localizedHour); | 
|  |  | 
|  | if (announce) { | 
|  | tryAnnounceForAccessibility(mHourView.getText(), true); | 
|  | } | 
|  | } | 
|  |  | 
|  | private void updateHeaderMinute(int minuteOfHour, boolean announce) { | 
|  | mMinuteView.setValue(minuteOfHour); | 
|  |  | 
|  | if (announce) { | 
|  | tryAnnounceForAccessibility(mMinuteView.getText(), false); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". | 
|  | * | 
|  | * See http://unicode.org/cldr/trac/browser/trunk/common/main | 
|  | * | 
|  | * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the | 
|  | * separator as the character which is just after the hour marker in the returned pattern. | 
|  | */ | 
|  | private void updateHeaderSeparator() { | 
|  | final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale, | 
|  | (mIs24Hour) ? "Hm" : "hm"); | 
|  | final String separatorText = getHourMinSeparatorFromPattern(bestDateTimePattern); | 
|  | mSeparatorView.setText(separatorText); | 
|  | mTextInputPickerView.updateSeparator(separatorText); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * This helper method extracts the time separator from the {@code datetimePattern}. | 
|  | * | 
|  | * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". | 
|  | * | 
|  | * See http://unicode.org/cldr/trac/browser/trunk/common/main | 
|  | * | 
|  | * @return Separator string. This is the character or set of quoted characters just after the | 
|  | * hour marker in {@code dateTimePattern}. Returns a colon (:) if it can't locate the | 
|  | * separator. | 
|  | * | 
|  | * @hide | 
|  | */ | 
|  | private static String getHourMinSeparatorFromPattern(String dateTimePattern) { | 
|  | final String defaultSeparator = ":"; | 
|  | boolean foundHourPattern = false; | 
|  | for (int i = 0; i < dateTimePattern.length(); i++) { | 
|  | switch (dateTimePattern.charAt(i)) { | 
|  | // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats. | 
|  | case 'H': | 
|  | case 'h': | 
|  | case 'K': | 
|  | case 'k': | 
|  | foundHourPattern = true; | 
|  | continue; | 
|  | case ' ': // skip spaces | 
|  | continue; | 
|  | case '\'': | 
|  | if (!foundHourPattern) { | 
|  | continue; | 
|  | } | 
|  | SpannableStringBuilder quotedSubstring = new SpannableStringBuilder( | 
|  | dateTimePattern.substring(i)); | 
|  | int quotedTextLength = DateFormat.appendQuotedText(quotedSubstring, 0); | 
|  | return quotedSubstring.subSequence(0, quotedTextLength).toString(); | 
|  | default: | 
|  | if (!foundHourPattern) { | 
|  | continue; | 
|  | } | 
|  | return Character.toString(dateTimePattern.charAt(i)); | 
|  | } | 
|  | } | 
|  | return defaultSeparator; | 
|  | } | 
|  |  | 
|  | static private int lastIndexOfAny(String str, char[] any) { | 
|  | final int lengthAny = any.length; | 
|  | if (lengthAny > 0) { | 
|  | for (int i = str.length() - 1; i >= 0; i--) { | 
|  | char c = str.charAt(i); | 
|  | for (int j = 0; j < lengthAny; j++) { | 
|  | if (c == any[j]) { | 
|  | return i; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | private void tryAnnounceForAccessibility(CharSequence text, boolean isHour) { | 
|  | if (mLastAnnouncedIsHour != isHour || !text.equals(mLastAnnouncedText)) { | 
|  | // TODO: Find a better solution, potentially live regions? | 
|  | mDelegator.announceForAccessibility(text); | 
|  | mLastAnnouncedText = text; | 
|  | mLastAnnouncedIsHour = isHour; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Show either Hours or Minutes. | 
|  | */ | 
|  | private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) { | 
|  | mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); | 
|  |  | 
|  | if (index == HOUR_INDEX) { | 
|  | if (announce) { | 
|  | mDelegator.announceForAccessibility(mSelectHours); | 
|  | } | 
|  | } else { | 
|  | if (announce) { | 
|  | mDelegator.announceForAccessibility(mSelectMinutes); | 
|  | } | 
|  | } | 
|  |  | 
|  | mHourView.setActivated(index == HOUR_INDEX); | 
|  | mMinuteView.setActivated(index == MINUTE_INDEX); | 
|  | } | 
|  |  | 
|  | private void setAmOrPm(int amOrPm) { | 
|  | updateAmPmLabelStates(amOrPm); | 
|  |  | 
|  | if (mRadialTimePickerView.setAmOrPm(amOrPm)) { | 
|  | mCurrentHour = getHour(); | 
|  | updateTextInputPicker(); | 
|  | if (mOnTimeChangedListener != null) { | 
|  | mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute()); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** Listener for RadialTimePickerView interaction. */ | 
|  | private final OnValueSelectedListener mOnValueSelectedListener = new OnValueSelectedListener() { | 
|  | @Override | 
|  | public void onValueSelected(int pickerType, int newValue, boolean autoAdvance) { | 
|  | boolean valueChanged = false; | 
|  | switch (pickerType) { | 
|  | case RadialTimePickerView.HOURS: | 
|  | if (getHour() != newValue) { | 
|  | valueChanged = true; | 
|  | } | 
|  | final boolean isTransition = mAllowAutoAdvance && autoAdvance; | 
|  | setHourInternal(newValue, FROM_RADIAL_PICKER, !isTransition, true); | 
|  | if (isTransition) { | 
|  | setCurrentItemShowing(MINUTE_INDEX, true, false); | 
|  |  | 
|  | final int localizedHour = getLocalizedHour(newValue); | 
|  | mDelegator.announceForAccessibility(localizedHour + ". " + mSelectMinutes); | 
|  | } | 
|  | break; | 
|  | case RadialTimePickerView.MINUTES: | 
|  | if (getMinute() != newValue) { | 
|  | valueChanged = true; | 
|  | } | 
|  | setMinuteInternal(newValue, FROM_RADIAL_PICKER, true); | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (mOnTimeChangedListener != null && valueChanged) { | 
|  | mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute()); | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | private final OnValueTypedListener mOnValueTypedListener = new OnValueTypedListener() { | 
|  | @Override | 
|  | public void onValueChanged(int pickerType, int newValue) { | 
|  | switch (pickerType) { | 
|  | case TextInputTimePickerView.HOURS: | 
|  | setHourInternal(newValue, FROM_INPUT_PICKER, false, true); | 
|  | break; | 
|  | case TextInputTimePickerView.MINUTES: | 
|  | setMinuteInternal(newValue, FROM_INPUT_PICKER, true); | 
|  | break; | 
|  | case TextInputTimePickerView.AMPM: | 
|  | setAmOrPm(newValue); | 
|  | break; | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | /** Listener for keyboard interaction. */ | 
|  | private final OnValueChangedListener mDigitEnteredListener = new OnValueChangedListener() { | 
|  | @Override | 
|  | public void onValueChanged(NumericTextView view, int value, | 
|  | boolean isValid, boolean isFinished) { | 
|  | final Runnable commitCallback; | 
|  | final View nextFocusTarget; | 
|  | if (view == mHourView) { | 
|  | commitCallback = mCommitHour; | 
|  | nextFocusTarget = view.isFocused() ? mMinuteView : null; | 
|  | } else if (view == mMinuteView) { | 
|  | commitCallback = mCommitMinute; | 
|  | nextFocusTarget = null; | 
|  | } else { | 
|  | return; | 
|  | } | 
|  |  | 
|  | view.removeCallbacks(commitCallback); | 
|  |  | 
|  | if (isValid) { | 
|  | if (isFinished) { | 
|  | // Done with hours entry, make visual updates | 
|  | // immediately and move to next focus if needed. | 
|  | commitCallback.run(); | 
|  |  | 
|  | if (nextFocusTarget != null) { | 
|  | nextFocusTarget.requestFocus(); | 
|  | } | 
|  | } else { | 
|  | // May still be making changes. Postpone visual | 
|  | // updates to prevent distracting the user. | 
|  | view.postDelayed(commitCallback, DELAY_COMMIT_MILLIS); | 
|  | } | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | private final Runnable mCommitHour = new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | setHour(mHourView.getValue()); | 
|  | } | 
|  | }; | 
|  |  | 
|  | private final Runnable mCommitMinute = new Runnable() { | 
|  | @Override | 
|  | public void run() { | 
|  | setMinute(mMinuteView.getValue()); | 
|  | } | 
|  | }; | 
|  |  | 
|  | private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() { | 
|  | @Override | 
|  | public void onFocusChange(View v, boolean focused) { | 
|  | if (focused) { | 
|  | switch (v.getId()) { | 
|  | case R.id.am_label: | 
|  | setAmOrPm(AM); | 
|  | break; | 
|  | case R.id.pm_label: | 
|  | setAmOrPm(PM); | 
|  | break; | 
|  | case R.id.hours: | 
|  | setCurrentItemShowing(HOUR_INDEX, true, true); | 
|  | break; | 
|  | case R.id.minutes: | 
|  | setCurrentItemShowing(MINUTE_INDEX, true, true); | 
|  | break; | 
|  | default: | 
|  | // Failed to handle this click, don't vibrate. | 
|  | return; | 
|  | } | 
|  |  | 
|  | tryVibrate(); | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  | private final View.OnClickListener mClickListener = new View.OnClickListener() { | 
|  | @Override | 
|  | public void onClick(View v) { | 
|  |  | 
|  | final int amOrPm; | 
|  | switch (v.getId()) { | 
|  | case R.id.am_label: | 
|  | setAmOrPm(AM); | 
|  | break; | 
|  | case R.id.pm_label: | 
|  | setAmOrPm(PM); | 
|  | break; | 
|  | case R.id.hours: | 
|  | setCurrentItemShowing(HOUR_INDEX, true, true); | 
|  | break; | 
|  | case R.id.minutes: | 
|  | setCurrentItemShowing(MINUTE_INDEX, true, true); | 
|  | break; | 
|  | default: | 
|  | // Failed to handle this click, don't vibrate. | 
|  | return; | 
|  | } | 
|  |  | 
|  | tryVibrate(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * Delegates unhandled touches in a view group to the nearest child view. | 
|  | */ | 
|  | private static class NearestTouchDelegate implements View.OnTouchListener { | 
|  | private View mInitialTouchTarget; | 
|  |  | 
|  | @Override | 
|  | public boolean onTouch(View view, MotionEvent motionEvent) { | 
|  | final int actionMasked = motionEvent.getActionMasked(); | 
|  | if (actionMasked == MotionEvent.ACTION_DOWN) { | 
|  | if (view instanceof ViewGroup) { | 
|  | mInitialTouchTarget = findNearestChild((ViewGroup) view, | 
|  | (int) motionEvent.getX(), (int) motionEvent.getY()); | 
|  | } else { | 
|  | mInitialTouchTarget = null; | 
|  | } | 
|  | } | 
|  |  | 
|  | final View child = mInitialTouchTarget; | 
|  | if (child == null) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | final float offsetX = view.getScrollX() - child.getLeft(); | 
|  | final float offsetY = view.getScrollY() - child.getTop(); | 
|  | motionEvent.offsetLocation(offsetX, offsetY); | 
|  | final boolean handled = child.dispatchTouchEvent(motionEvent); | 
|  | motionEvent.offsetLocation(-offsetX, -offsetY); | 
|  |  | 
|  | if (actionMasked == MotionEvent.ACTION_UP | 
|  | || actionMasked == MotionEvent.ACTION_CANCEL) { | 
|  | mInitialTouchTarget = null; | 
|  | } | 
|  |  | 
|  | return handled; | 
|  | } | 
|  |  | 
|  | private View findNearestChild(ViewGroup v, int x, int y) { | 
|  | View bestChild = null; | 
|  | int bestDist = Integer.MAX_VALUE; | 
|  |  | 
|  | for (int i = 0, count = v.getChildCount(); i < count; i++) { | 
|  | final View child = v.getChildAt(i); | 
|  | final int dX = x - (child.getLeft() + child.getWidth() / 2); | 
|  | final int dY = y - (child.getTop() + child.getHeight() / 2); | 
|  | final int dist = dX * dX + dY * dY; | 
|  | if (bestDist > dist) { | 
|  | bestChild = child; | 
|  | bestDist = dist; | 
|  | } | 
|  | } | 
|  |  | 
|  | return bestChild; | 
|  | } | 
|  | } | 
|  | } |