diff options
| -rw-r--r-- | core/java/android/util/IntArray.java | 162 | ||||
| -rw-r--r-- | core/java/android/widget/RadialTimePickerView.java | 538 | ||||
| -rw-r--r-- | core/java/android/widget/SimpleMonthView.java | 3 | ||||
| -rw-r--r-- | core/java/android/widget/TimePickerClockDelegate.java | 20 | ||||
| -rw-r--r-- | core/java/com/android/internal/widget/ExploreByTouchHelper.java | 85 | ||||
| -rw-r--r-- | core/res/res/layout/time_header_label.xml | 8 | 
6 files changed, 626 insertions, 190 deletions
| diff --git a/core/java/android/util/IntArray.java b/core/java/android/util/IntArray.java new file mode 100644 index 000000000000..e8d394747b2a --- /dev/null +++ b/core/java/android/util/IntArray.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2014 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.util; + +import com.android.internal.util.ArrayUtils; + +import libcore.util.EmptyArray; + +/** + * Implements a growing array of int primitives. + * + * @hide + */ +public class IntArray implements Cloneable { +    private static final int MIN_CAPACITY_INCREMENT = 12; + +    private int[] mValues; +    private int mSize; + +    /** +     * Creates an empty IntArray with the default initial capacity. +     */ +    public IntArray() { +        this(10); +    } + +    /** +     * Creates an empty IntArray with the specified initial capacity. +     */ +    public IntArray(int initialCapacity) { +        if (initialCapacity == 0) { +            mValues = EmptyArray.INT; +        } else { +            mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity); +        } +        mSize = 0; +    } + +    /** +     * Appends the specified value to the end of this array. +     */ +    public void add(int value) { +        add(mSize, value); +    } + +    /** +     * Inserts a value at the specified position in this array. +     * +     * @throws IndexOutOfBoundsException when index < 0 || index > size() +     */ +    public void add(int index, int value) { +        if (index < 0 || index > mSize) { +            throw new IndexOutOfBoundsException(); +        } + +        ensureCapacity(1); + +        if (mSize - index != 0) { +            System.arraycopy(mValues, index, mValues, index + 1, mSize - index); +        } + +        mValues[index] = value; +        mSize++; +    } + +    /** +     * Adds the values in the specified array to this array. +     */ +    public void addAll(IntArray values) { +        final int count = values.mSize; +        ensureCapacity(count); + +        System.arraycopy(values.mValues, 0, mValues, mSize, count); +        mSize += count; +    } + +    /** +     * Ensures capacity to append at least <code>count</code> values. +     */ +    private void ensureCapacity(int count) { +        final int currentSize = mSize; +        final int minCapacity = currentSize + count; +        if (minCapacity >= mValues.length) { +            final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ? +                    MIN_CAPACITY_INCREMENT : currentSize >> 1); +            final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity; +            final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity); +            System.arraycopy(mValues, 0, newValues, 0, currentSize); +            mValues = newValues; +        } +    } + +    /** +     * Removes all values from this array. +     */ +    public void clear() { +        mSize = 0; +    } + +    @Override +    public IntArray clone() throws CloneNotSupportedException { +        final IntArray clone = (IntArray) super.clone(); +        clone.mValues = mValues.clone(); +        return clone; +    } + +    /** +     * Returns the value at the specified position in this array. +     */ +    public int get(int index) { +        if (index >= mSize) { +            throw new ArrayIndexOutOfBoundsException(mSize, index); +        } +        return mValues[index]; +    } + +    /** +     * Returns the index of the first occurrence of the specified value in this +     * array, or -1 if this array does not contain the value. +     */ +    public int indexOf(int value) { +        final int n = mSize; +        for (int i = 0; i < n; i++) { +            if (mValues[i] == value) { +                return i; +            } +        } +        return -1; +    } + +    /** +     * Removes the value at the specified index from this array. +     */ +    public void remove(int index) { +        if (index >= mSize) { +            throw new ArrayIndexOutOfBoundsException(mSize, index); +        } +        System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1); +        mSize--; +    } + +    /** +     * Returns the number of values in this array. +     */ +    public int size() { +        return mSize; +    } +} diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java index d15f2d6e9e08..24fc2bb9a8d2 100644 --- a/core/java/android/widget/RadialTimePickerView.java +++ b/core/java/android/widget/RadialTimePickerView.java @@ -28,13 +28,13 @@ import android.content.res.TypedArray;  import android.graphics.Canvas;  import android.graphics.Color;  import android.graphics.Paint; +import android.graphics.Rect;  import android.graphics.Typeface; -import android.graphics.RectF;  import android.os.Bundle; -import android.text.format.DateUtils; -import android.text.format.Time;  import android.util.AttributeSet; +import android.util.IntArray;  import android.util.Log; +import android.util.MathUtils;  import android.util.TypedValue;  import android.view.HapticFeedbackConstants;  import android.view.MotionEvent; @@ -42,8 +42,10 @@ import android.view.View;  import android.view.ViewGroup;  import android.view.accessibility.AccessibilityEvent;  import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;  import com.android.internal.R; +import com.android.internal.widget.ExploreByTouchHelper;  import java.util.ArrayList;  import java.util.Calendar; @@ -97,6 +99,9 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {      private static int[] sSnapPrefer30sMap = new int[361]; +    private final InvalidateUpdateListener mInvalidateUpdateListener = +            new InvalidateUpdateListener(); +      private final String[] mHours12Texts = new String[12];      private final String[] mOuterHours24Texts = new String[12];      private final String[] mInnerHours24Texts = new String[12]; @@ -115,7 +120,39 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {      private final Paint mPaintBackground = new Paint();      private final Paint mPaintDebug = new Paint(); -    private Typeface mTypeface; +    private final Typeface mTypeface; + +    private final float[] mCircleRadius = new float[3]; + +    private final float[] mTextSize = new float[2]; + +    private final float[][] mTextGridHeights = new float[2][7]; +    private final float[][] mTextGridWidths = new float[2][7]; + +    private final float[] mInnerTextGridHeights = new float[7]; +    private final float[] mInnerTextGridWidths = new float[7]; + +    private final float[] mCircleRadiusMultiplier = new float[2]; +    private final float[] mNumbersRadiusMultiplier = new float[3]; + +    private final float[] mTextSizeMultiplier = new float[3]; + +    private final float[] mAnimationRadiusMultiplier = new float[3]; + +    private final float mTransitionMidRadiusMultiplier; +    private final float mTransitionEndRadiusMultiplier; + +    private final int[] mLineLength = new int[3]; +    private final int[] mSelectionRadius = new int[3]; +    private final float mSelectionRadiusMultiplier; +    private final int[] mSelectionDegrees = new int[3]; + +    private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>(); +    private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>(); + +    private final RadialPickerTouchHelper mTouchHelper; + +    private float mInnerTextSize;      private boolean mIs24HourMode;      private boolean mShowHours; @@ -129,52 +166,21 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {      private int mXCenter;      private int mYCenter; -    private float[] mCircleRadius = new float[3]; -      private int mMinHypotenuseForInnerNumber;      private int mMaxHypotenuseForOuterNumber;      private int mHalfwayHypotenusePoint; -    private float[] mTextSize = new float[2]; -    private float mInnerTextSize; - -    private float[][] mTextGridHeights = new float[2][7]; -    private float[][] mTextGridWidths = new float[2][7]; - -    private float[] mInnerTextGridHeights = new float[7]; -    private float[] mInnerTextGridWidths = new float[7]; -      private String[] mOuterTextHours;      private String[] mInnerTextHours;      private String[] mOuterTextMinutes; - -    private float[] mCircleRadiusMultiplier = new float[2]; -    private float[] mNumbersRadiusMultiplier = new float[3]; - -    private float[] mTextSizeMultiplier = new float[3]; - -    private float[] mAnimationRadiusMultiplier = new float[3]; - -    private float mTransitionMidRadiusMultiplier; -    private float mTransitionEndRadiusMultiplier; -      private AnimatorSet mTransition; -    private InvalidateUpdateListener mInvalidateUpdateListener = new InvalidateUpdateListener(); - -    private int[] mLineLength = new int[3]; -    private int[] mSelectionRadius = new int[3]; -    private float mSelectionRadiusMultiplier; -    private int[] mSelectionDegrees = new int[3];      private int mAmOrPm;      private int mDisabledAlpha; -    private RectF mRectF = new RectF(); -    private boolean mInputEnabled = true;      private OnValueSelectedListener mListener; -    private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>(); -    private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>(); +    private boolean mInputEnabled = true;      public interface OnValueSelectedListener {          void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); @@ -282,11 +288,21 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          return degrees;      } +    @SuppressWarnings("unused") +    public RadialTimePickerView(Context context)  { +        this(context, null); +    } +      public RadialTimePickerView(Context context, AttributeSet attrs)  {          this(context, attrs, R.attr.timePickerStyle);      } -    public RadialTimePickerView(Context context, AttributeSet attrs, int defStyle)  { +    public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr)  { +        this(context, attrs, defStyleAttr, 0); +    } + +    public RadialTimePickerView( +            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)  {          super(context, attrs);          // Pull disabled alpha from theme. @@ -297,7 +313,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          // process style attributes          final Resources res = getResources();          final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker, -                defStyle, 0); +                defStyleAttr, defStyleRes);          mTypeface = Typeface.create("sans-serif", Typeface.NORMAL); @@ -382,6 +398,14 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          mIs24HourMode = false;          mAmOrPm = AM; +        // Set up accessibility components. +        mTouchHelper = new RadialPickerTouchHelper(); +        setAccessibilityDelegate(mTouchHelper); + +        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { +            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); +        } +          initHoursAndMinutesText();          initData(); @@ -406,8 +430,8 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);          final int currentMinute = calendar.get(Calendar.MINUTE); -        setCurrentHour(currentHour); -        setCurrentMinute(currentMinute); +        setCurrentHourInternal(currentHour, false, false); +        setCurrentMinuteInternal(currentMinute, false);          setHapticFeedbackEnabled(true);      } @@ -429,8 +453,9 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {      public void initialize(int hour, int minute, boolean is24HourMode) {          mIs24HourMode = is24HourMode; -        setCurrentHour(hour); -        setCurrentMinute(minute); + +        setCurrentHourInternal(hour, false, false); +        setCurrentMinuteInternal(minute, false);      }      public void setCurrentItemShowing(int item, boolean animate) { @@ -460,17 +485,39 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {       * @param hour the current hour between 0 and 23 (inclusive)       */      public void setCurrentHour(int hour) { +        setCurrentHourInternal(hour, true, false); +    } + +    /** +     * Sets the current hour. +     * +     * @param hour The current hour +     * @param callback Whether the value listener should be invoked +     * @param autoAdvance Whether the listener should auto-advance to the next +     *                    selection mode, e.g. hour to minutes +     */ +    private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) {          final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR;          mSelectionDegrees[HOURS] = degrees;          mSelectionDegrees[HOURS_INNER] = degrees;          // 0 is 12 AM (midnight) and 12 is 12 PM (noon). -        mAmOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM; -        mIsOnInnerCircle = mIs24HourMode && hour >= 1 && hour <= 12; +        final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM; +        final boolean isOnInnerCircle = mIs24HourMode && hour >= 1 && hour <= 12; +        if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) { +            mAmOrPm = amOrPm; +            mIsOnInnerCircle = isOnInnerCircle; + +            initData(); +            updateLayoutData(); +            mTouchHelper.invalidateRoot(); +        } -        initData(); -        updateLayoutData();          invalidate(); + +        if (callback && mListener != null) { +            mListener.onValueSelected(HOURS, hour, autoAdvance); +        }      }      /** @@ -479,15 +526,19 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {       * @return the current hour between 0 and 23 (inclusive)       */      public int getCurrentHour() { -        int hour = (mSelectionDegrees[mIsOnInnerCircle ? -                HOURS_INNER : HOURS] / DEGREES_FOR_ONE_HOUR) % 12; +        return getHourForDegrees( +                mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS], mIsOnInnerCircle); +    } + +    private int getHourForDegrees(int degrees, boolean innerCircle) { +        int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12;          if (mIs24HourMode) {              // Convert the 12-hour value into 24-hour time based on where the              // selector is positioned. -            if (mIsOnInnerCircle && hour == 0) { +            if (innerCircle && hour == 0) {                  // Inner circle is 1 through 12.                  hour = 12; -            } else if (!mIsOnInnerCircle && hour != 0) { +            } else if (!innerCircle && hour != 0) {                  // Outer circle is 13 through 23 and 0.                  hour += 12;              } @@ -497,19 +548,49 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          return hour;      } +    private int getDegreesForHour(int hour) { +        // Convert to be 0-11. +        if (mIs24HourMode) { +            if (hour >= 12) { +                hour -= 12; +            } +        } else if (hour == 12) { +            hour = 0; +        } +        return hour * DEGREES_FOR_ONE_HOUR; +    } +      public void setCurrentMinute(int minute) { +        setCurrentMinuteInternal(minute, true); +    } + +    private void setCurrentMinuteInternal(int minute, boolean callback) {          mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE; +          invalidate(); + +        if (callback && mListener != null) { +            mListener.onValueSelected(MINUTES, minute, false); +        }      }      // Returns minutes in 0-59 range      public int getCurrentMinute() { -        return (mSelectionDegrees[MINUTES] / DEGREES_FOR_ONE_MINUTE); +        return getMinuteForDegrees(mSelectionDegrees[MINUTES]); +    } + +    private int getMinuteForDegrees(int degrees) { +        return degrees / DEGREES_FOR_ONE_MINUTE; +    } + +    private int getDegreesForMinute(int minute) { +        return minute * DEGREES_FOR_ONE_MINUTE;      }      public void setAmOrPm(int val) {          mAmOrPm = (val % 2);          invalidate(); +        mTouchHelper.invalidateRoot();      }      public int getAmOrPm() { @@ -648,6 +729,8 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          mSelectionRadius[HOURS] = (int) (mCircleRadius[HOURS] * mSelectionRadiusMultiplier);          mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS];          mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier); + +        mTouchHelper.invalidateRoot();      }      @Override @@ -769,20 +852,17 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          float top = mYCenter - outerRadius;          float right = mXCenter + outerRadius;          float bottom = mYCenter + outerRadius; -        mRectF = new RectF(left, top, right, bottom); -        canvas.drawRect(mRectF, mPaintDebug); +        canvas.drawRect(left, top, right, bottom, mPaintDebug);          // Draw outer rectangle for background          left = mXCenter - mCircleRadius[HOURS];          top = mYCenter - mCircleRadius[HOURS];          right = mXCenter + mCircleRadius[HOURS];          bottom = mYCenter + mCircleRadius[HOURS]; -        mRectF.set(left, top, right, bottom); -        canvas.drawRect(mRectF, mPaintDebug); +        canvas.drawRect(left, top, right, bottom, mPaintDebug);          // Draw outer view rectangle -        mRectF.set(0, 0, getWidth(), getHeight()); -        canvas.drawRect(mRectF, mPaintDebug); +        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaintDebug);          // Draw selected time          final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute()); @@ -896,12 +976,14 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {      }      // Used for animating the hours by changing their radius +    @SuppressWarnings("unused")      private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) {          mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier;          mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier;      }      // Used for animating the minutes by changing their radius +    @SuppressWarnings("unused")      private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) {          mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier;      } @@ -1094,21 +1176,25 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          }          final float opposite = Math.abs(y - mYCenter); -        double degrees = Math.toDegrees(Math.asin(opposite / hypotenuse)); +        int degrees = (int) (Math.toDegrees(Math.asin(opposite / hypotenuse)) + 0.5);          // Now we have to translate to the correct quadrant. -        boolean rightSide = (x > mXCenter); -        boolean topSide = (y < mYCenter); -        if (rightSide && topSide) { -            degrees = 90 - degrees; -        } else if (rightSide && !topSide) { -            degrees = 90 + degrees; -        } else if (!rightSide && !topSide) { -            degrees = 270 - degrees; -        } else if (!rightSide && topSide) { -            degrees = 270 + degrees; +        final boolean rightSide = (x > mXCenter); +        final boolean topSide = (y < mYCenter); +        if (rightSide) { +            if (topSide) { +                degrees = 90 - degrees; +            } else { +                degrees = 90 + degrees; +            } +        } else { +            if (topSide) { +                degrees = 270 + degrees; +            } else { +                degrees = 270 - degrees; +            }          } -        return (int) degrees; +        return degrees;      }      @Override @@ -1176,109 +1262,277 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {          return result;      } -    /** -     * Necessary for accessibility, to ensure we support "scrolling" forward and backward -     * in the circle. -     */ -    @Override -    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { -        super.onInitializeAccessibilityNodeInfo(info); -        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); -        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); -    } - -    /** -     * Announce the currently-selected time when launched. -     */      @Override -    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { -        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { -            // Clear the event's current text so that only the current time will be spoken. -            event.getText().clear(); -            Time time = new Time(); -            time.hour = getCurrentHour(); -            time.minute = getCurrentMinute(); -            long millis = time.normalize(true); -            int flags = DateUtils.FORMAT_SHOW_TIME; -            if (mIs24HourMode) { -                flags |= DateUtils.FORMAT_24HOUR; -            } -            String timeString = DateUtils.formatDateTime(getContext(), millis, flags); -            event.getText().add(timeString); +    public boolean dispatchHoverEvent(MotionEvent event) { +        // First right-of-refusal goes the touch exploration helper. +        if (mTouchHelper.dispatchHoverEvent(event)) {              return true;          } -        return super.dispatchPopulateAccessibilityEvent(event); +        return super.dispatchHoverEvent(event);      } -    /** -     * When scroll forward/backward events are received, jump the time to the higher/lower -     * discrete, visible value on the circle. -     */ -    @Override -    public boolean performAccessibilityAction(int action, Bundle arguments) { -        if (super.performAccessibilityAction(action, arguments)) { -            return true; +    public void setInputEnabled(boolean inputEnabled) { +        mInputEnabled = inputEnabled; +        invalidate(); +    } + +    private class RadialPickerTouchHelper extends ExploreByTouchHelper { +        private final Rect mTempRect = new Rect(); + +        private final int TYPE_HOUR = 1; +        private final int TYPE_MINUTE = 2; + +        private final int SHIFT_TYPE = 0; +        private final int MASK_TYPE = 0xF; + +        private final int SHIFT_VALUE = 8; +        private final int MASK_VALUE = 0xFF; + +        /** Increment in which virtual views are exposed for minutes. */ +        private final int MINUTE_INCREMENT = 5; + +        public RadialPickerTouchHelper() { +            super(RadialTimePickerView.this);          } -        int changeMultiplier = 0; -        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { -            changeMultiplier = 1; -        } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { -            changeMultiplier = -1; +        @Override +        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { +            super.onInitializeAccessibilityNodeInfo(host, info); + +            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); +            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);          } -        if (changeMultiplier != 0) { -            int value; -            final int stepSize; -            if (mShowHours) { -                stepSize = DEGREES_FOR_ONE_HOUR; -                value = getCurrentHour() % 12; -            } else { -                stepSize = DEGREES_FOR_ONE_MINUTE; -                value = getCurrentMinute(); + +        @Override +        public boolean performAccessibilityAction(View host, int action, Bundle arguments) { +            if (super.performAccessibilityAction(host, action, arguments)) { +                return true;              } -            int degrees = value * stepSize; -            degrees = snapOnly30s(degrees, changeMultiplier); -            value = degrees / stepSize; +            switch (action) { +                case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: +                    adjustPicker(1); +                    return true; +                case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: +                    adjustPicker(-1); +                    return true; +            } + +            return false; +        } + +        private void adjustPicker(int step) { +            final int stepSize; +            final int initialValue;              final int maxValue; -            int minValue = 0; +            final int minValue;              if (mShowHours) { +                stepSize = DEGREES_FOR_ONE_HOUR; +                initialValue = getCurrentHour() % 12; +                  if (mIs24HourMode) {                      maxValue = 23; +                    minValue = 0;                  } else {                      maxValue = 12;                      minValue = 1;                  }              } else { +                stepSize = DEGREES_FOR_ONE_MINUTE; +                initialValue = getCurrentMinute(); +                  maxValue = 55; +                minValue = 0;              } -            if (value > maxValue) { -                // If we scrolled forward past the highest number, wrap around to the lowest. -                value = minValue; -            } else if (value < minValue) { -                // If we scrolled backward past the lowest number, wrap around to the highest. -                value = maxValue; + +            final int steppedValue = snapOnly30s(initialValue * stepSize, step) / stepSize; +            final int clampedValue = MathUtils.constrain(steppedValue, minValue, maxValue); +            if (mShowHours) { +                setCurrentHour(clampedValue); +            } else { +                setCurrentMinute(clampedValue); +            } +        } + +        @Override +        protected int getVirtualViewAt(float x, float y) { +            final int id; +            final int degrees = getDegreesFromXY(x, y); +            if (degrees != -1) { +                final int snapDegrees = snapOnly30s(degrees, 0) % 360; +                if (mShowHours) { +                    final int hour = getHourForDegrees(snapDegrees, mIsOnInnerCircle); +                    id = makeId(TYPE_HOUR, hour); +                } else { +                    final int current = getCurrentMinute(); +                    final int touched = getMinuteForDegrees(degrees); +                    final int snapped = getMinuteForDegrees(snapDegrees); + +                    // If the touched minute is closer to the current minute +                    // than it is to the snapped minute, return current. +                    final int minute; +                    if (Math.abs(current - touched) < Math.abs(snapped - touched)) { +                        minute = current; +                    } else { +                        minute = snapped; +                    } +                    id = makeId(TYPE_MINUTE, minute); +                } +            } else { +                id = INVALID_ID;              } + +            return id; +        } + +        @Override +        protected void getVisibleVirtualViews(IntArray virtualViewIds) {              if (mShowHours) { -                setCurrentHour(value); -                if (mListener != null) { -                    mListener.onValueSelected(HOURS, value, false); +                final int min = mIs24HourMode ? 0 : 1; +                final int max = mIs24HourMode ? 23 : 12; +                for (int i = min; i <= max ; i++) { +                    virtualViewIds.add(makeId(TYPE_HOUR, i));                  }              } else { -                setCurrentMinute(value); -                if (mListener != null) { -                    mListener.onValueSelected(MINUTES, value, false); +                final int current = getCurrentMinute(); +                for (int i = 0; i < 60; i += MINUTE_INCREMENT) { +                    virtualViewIds.add(makeId(TYPE_MINUTE, i)); + +                    // If the current minute falls between two increments, +                    // insert an extra node for it. +                    if (current > i && current < i + MINUTE_INCREMENT) { +                        virtualViewIds.add(makeId(TYPE_MINUTE, current)); +                    }                  }              } -            return true;          } -        return false; -    } +        @Override +        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { +            event.setClassName(getClass().getName()); -    public void setInputEnabled(boolean inputEnabled) { -        mInputEnabled = inputEnabled; -        invalidate(); +            final int type = getTypeFromId(virtualViewId); +            final int value = getValueFromId(virtualViewId); +            final CharSequence description = getVirtualViewDescription(type, value); +            event.setContentDescription(description); +        } + +        @Override +        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { +            node.setClassName(getClass().getName()); +            node.addAction(AccessibilityAction.ACTION_CLICK); + +            final int type = getTypeFromId(virtualViewId); +            final int value = getValueFromId(virtualViewId); +            final CharSequence description = getVirtualViewDescription(type, value); +            node.setContentDescription(description); + +            getBoundsForVirtualView(virtualViewId, mTempRect); +            node.setBoundsInParent(mTempRect); + +            final boolean selected = isVirtualViewSelected(type, value); +            node.setSelected(selected); +        } + +        @Override +        protected boolean onPerformActionForVirtualView(int virtualViewId, int action, +                Bundle arguments) { +            if (action == AccessibilityNodeInfo.ACTION_CLICK) { +                final int type = getTypeFromId(virtualViewId); +                final int value = getValueFromId(virtualViewId); +                if (type == TYPE_HOUR) { +                    final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm); +                    setCurrentHour(hour); +                    return true; +                } else if (type == TYPE_MINUTE) { +                    setCurrentMinute(value); +                    return true; +                } +            } +            return false; +        } + +        private int hour12To24(int hour12, int amOrPm) { +            int hour24 = hour12; +            if (hour12 == 12) { +                if (amOrPm == AM) { +                    hour24 = 0; +                } +            } else if (amOrPm == PM) { +                hour24 += 12; +            } +            return hour24; +        } + +        private void getBoundsForVirtualView(int virtualViewId, Rect bounds) { +            final float radius; +            final int type = getTypeFromId(virtualViewId); +            final int value = getValueFromId(virtualViewId); +            final float centerRadius; +            final float degrees; +            if (type == TYPE_HOUR) { +                final boolean innerCircle = mIs24HourMode && value > 0 && value <= 12; +                if (innerCircle) { +                    centerRadius = mCircleRadius[HOURS_INNER] * mNumbersRadiusMultiplier[HOURS_INNER]; +                    radius = mSelectionRadius[HOURS_INNER]; +                } else { +                    centerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS]; +                    radius = mSelectionRadius[HOURS]; +                } + +                degrees = getDegreesForHour(value); +            } else if (type == TYPE_MINUTE) { +                centerRadius = mCircleRadius[MINUTES] * mNumbersRadiusMultiplier[MINUTES]; +                degrees = getDegreesForMinute(value); +                radius = mSelectionRadius[MINUTES]; +            } else { +                // This should never happen. +                centerRadius = 0; +                degrees = 0; +                radius = 0; +            } + +            final double radians = Math.toRadians(degrees); +            final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians); +            final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians); + +            bounds.set((int) (xCenter - radius), (int) (yCenter - radius), +                    (int) (xCenter + radius), (int) (yCenter + radius)); +        } + +        private CharSequence getVirtualViewDescription(int type, int value) { +            final CharSequence description; +            if (type == TYPE_HOUR || type == TYPE_MINUTE) { +                description = Integer.toString(value); +            } else { +                description = null; +            } +            return description; +        } + +        private boolean isVirtualViewSelected(int type, int value) { +            final boolean selected; +            if (type == TYPE_HOUR) { +                selected = getCurrentHour() == value; +            } else if (type == TYPE_MINUTE) { +                selected = getCurrentMinute() == value; +            } else { +                selected = false; +            } +            return selected; +        } + +        private int makeId(int type, int value) { +            return type << SHIFT_TYPE | value << SHIFT_VALUE; +        } + +        private int getTypeFromId(int id) { +            return id >>> SHIFT_TYPE & MASK_TYPE; +        } + +        private int getValueFromId(int id) { +            return id >>> SHIFT_VALUE & MASK_VALUE; +        }      }      private static class IntHolder { diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java index a76241ed1a50..59baabaebae5 100644 --- a/core/java/android/widget/SimpleMonthView.java +++ b/core/java/android/widget/SimpleMonthView.java @@ -31,6 +31,7 @@ import android.text.format.DateFormat;  import android.text.format.DateUtils;  import android.text.format.Time;  import android.util.AttributeSet; +import android.util.IntArray;  import android.util.MathUtils;  import android.view.MotionEvent;  import android.view.View; @@ -610,7 +611,7 @@ class SimpleMonthView extends View {          }          @Override -        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { +        protected void getVisibleVirtualViews(IntArray virtualViewIds) {              for (int day = 1; day <= mNumCells; day++) {                  virtualViewIds.add(day);              } diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java index eca304800cb8..78ee2471dcb1 100644 --- a/core/java/android/widget/TimePickerClockDelegate.java +++ b/core/java/android/widget/TimePickerClockDelegate.java @@ -611,15 +611,12 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl              if (mAllowAutoAdvance && autoAdvance) {                  updateHeaderHour(newValue, false);                  setCurrentItemShowing(MINUTE_INDEX, true, false); -                mRadialTimePickerView.announceForAccessibility(newValue + ". " + mSelectMinutes); +                mDelegator.announceForAccessibility(newValue + ". " + mSelectMinutes);              } else {                  updateHeaderHour(newValue, true); -                mRadialTimePickerView.setContentDescription( -                        mHourPickerDescription + ": " + newValue);              }          } else if (pickerIndex == MINUTE_INDEX){              updateHeaderMinute(newValue, true); -            mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + newValue);          } else if (pickerIndex == AMPM_INDEX) {              updateAmPmLabelStates(newValue);          } else if (pickerIndex == ENABLE_PICKER_INDEX) { @@ -744,19 +741,12 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl          mRadialTimePickerView.setCurrentItemShowing(index, animateCircle);          if (index == HOUR_INDEX) { -            int hours = mRadialTimePickerView.getCurrentHour(); -            if (!mIs24HourView) { -                hours = hours % 12; -            } -            mRadialTimePickerView.setContentDescription(mHourPickerDescription + ": " + hours);              if (announce) { -                mRadialTimePickerView.announceForAccessibility(mSelectHours); +                mDelegator.announceForAccessibility(mSelectHours);              }          } else { -            int minutes = mRadialTimePickerView.getCurrentMinute(); -            mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes);              if (announce) { -                mRadialTimePickerView.announceForAccessibility(mSelectMinutes); +                mDelegator.announceForAccessibility(mSelectMinutes);              }          } @@ -789,7 +779,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl                      } else {                          deletedKeyStr = String.format("%d", getValFromKeyCode(deleted));                      } -                    mRadialTimePickerView.announceForAccessibility( +                    mDelegator.announceForAccessibility(                              String.format(mDeletedKeyFormat, deletedKeyStr));                      updateDisplay(true);                  } @@ -851,7 +841,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl          }          int val = getValFromKeyCode(keyCode); -        mRadialTimePickerView.announceForAccessibility(String.format("%d", val)); +        mDelegator.announceForAccessibility(String.format("%d", val));          // Automatically fill in 0's if AM or PM was legally entered.          if (isTypedTimeFullyLegal()) {              if (!mIs24HourView && mTypedTimes.size() <= 3) { diff --git a/core/java/com/android/internal/widget/ExploreByTouchHelper.java b/core/java/com/android/internal/widget/ExploreByTouchHelper.java index 4689179835cc..0e046cb21609 100644 --- a/core/java/com/android/internal/widget/ExploreByTouchHelper.java +++ b/core/java/com/android/internal/widget/ExploreByTouchHelper.java @@ -19,6 +19,7 @@ package com.android.internal.widget;  import android.content.Context;  import android.graphics.Rect;  import android.os.Bundle; +import android.util.IntArray;  import android.view.accessibility.*;  import android.view.MotionEvent;  import android.view.View; @@ -26,11 +27,9 @@ import android.view.ViewParent;  import android.view.accessibility.AccessibilityEvent;  import android.view.accessibility.AccessibilityManager;  import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;  import android.view.accessibility.AccessibilityNodeProvider; -import java.util.LinkedList; -import java.util.List; -  /**   * ExploreByTouchHelper is a utility class for implementing accessibility   * support in custom {@link android.view.View}s that represent a collection of View-like @@ -58,14 +57,16 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {      private static final Rect INVALID_PARENT_BOUNDS = new Rect(              Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); -    // Temporary, reusable data structures. -    private final Rect mTempScreenRect = new Rect(); -    private final Rect mTempParentRect = new Rect(); -    private final Rect mTempVisibleRect = new Rect(); -    private final int[] mTempGlobalRect = new int[2]; +    // Lazily-created temporary data structures used when creating nodes. +    private Rect mTempScreenRect; +    private Rect mTempParentRect; +    private int[] mTempGlobalRect; + +    /** Lazily-created temporary data structure used to compute visibility. */ +    private Rect mTempVisibleRect; -    /** View's context **/ -    private Context mContext; +    /** Lazily-created temporary data structure used to obtain child IDs. */ +    private IntArray mTempArray;      /** System accessibility manager, used to check state and send events. */      private final AccessibilityManager mManager; @@ -73,6 +74,9 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {      /** View whose internal structure is exposed through this helper. */      private final View mView; +    /** Context of the host view. **/ +    private final Context mContext; +      /** Node provider that handles creating nodes and performing actions. */      private ExploreByTouchNodeProvider mNodeProvider; @@ -332,11 +336,17 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {          onInitializeAccessibilityNodeInfo(mView, node);          // Add the virtual descendants. -        final LinkedList<Integer> virtualViewIds = new LinkedList<Integer>(); +        if (mTempArray == null) { +            mTempArray = new IntArray(); +        } else { +            mTempArray.clear(); +        } +        final IntArray virtualViewIds = mTempArray;          getVisibleVirtualViews(virtualViewIds); -        for (Integer childVirtualViewId : virtualViewIds) { -            node.addChild(mView, childVirtualViewId); +        final int N = virtualViewIds.size(); +        for (int i = 0; i < N; i++) { +            node.addChild(mView, virtualViewIds.get(i));          }          return node; @@ -371,6 +381,11 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {       * @return An {@link AccessibilityNodeInfo} for the specified item.       */      private AccessibilityNodeInfo createNodeForChild(int virtualViewId) { +        ensureTempRects(); +        final Rect tempParentRect = mTempParentRect; +        final int[] tempGlobalRect = mTempGlobalRect; +        final Rect tempScreenRect = mTempScreenRect; +          final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();          // Ensure the client has good defaults. @@ -387,8 +402,8 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {                      + "populateNodeForVirtualViewId()");          } -        node.getBoundsInParent(mTempParentRect); -        if (mTempParentRect.equals(INVALID_PARENT_BOUNDS)) { +        node.getBoundsInParent(tempParentRect); +        if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) {              throw new RuntimeException("Callbacks must set parent bounds in "                      + "populateNodeForVirtualViewId()");          } @@ -411,29 +426,35 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {          // Manage internal accessibility focus state.          if (mFocusedVirtualViewId == virtualViewId) {              node.setAccessibilityFocused(true); -            node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); +            node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);          } else {              node.setAccessibilityFocused(false); -            node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); +            node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);          }          // Set the visibility based on the parent bound. -        if (intersectVisibleToUser(mTempParentRect)) { +        if (intersectVisibleToUser(tempParentRect)) {              node.setVisibleToUser(true); -            node.setBoundsInParent(mTempParentRect); +            node.setBoundsInParent(tempParentRect);          }          // Calculate screen-relative bound. -        mView.getLocationOnScreen(mTempGlobalRect); -        final int offsetX = mTempGlobalRect[0]; -        final int offsetY = mTempGlobalRect[1]; -        mTempScreenRect.set(mTempParentRect); -        mTempScreenRect.offset(offsetX, offsetY); -        node.setBoundsInScreen(mTempScreenRect); +        mView.getLocationOnScreen(tempGlobalRect); +        final int offsetX = tempGlobalRect[0]; +        final int offsetY = tempGlobalRect[1]; +        tempScreenRect.set(tempParentRect); +        tempScreenRect.offset(offsetX, offsetY); +        node.setBoundsInScreen(tempScreenRect);          return node;      } +    private void ensureTempRects() { +        mTempGlobalRect = new int[2]; +        mTempParentRect = new Rect(); +        mTempScreenRect = new Rect(); +    } +      private boolean performAction(int virtualViewId, int action, Bundle arguments) {          switch (virtualViewId) {              case View.NO_ID: @@ -451,13 +472,13 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {          switch (action) {              case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:              case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: -                return manageFocusForChild(virtualViewId, action, arguments); +                return manageFocusForChild(virtualViewId, action);              default:                  return onPerformActionForVirtualView(virtualViewId, action, arguments);          }      } -    private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) { +    private boolean manageFocusForChild(int virtualViewId, int action) {          switch (action) {              case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:                  return requestAccessibilityFocus(virtualViewId); @@ -503,12 +524,16 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {          }          // If no portion of the parent is visible, this view is not visible. -        if (!mView.getLocalVisibleRect(mTempVisibleRect)) { +        if (mTempVisibleRect == null) { +            mTempVisibleRect = new Rect(); +        } +        final Rect tempVisibleRect = mTempVisibleRect; +        if (!mView.getLocalVisibleRect(tempVisibleRect)) {              return false;          }          // Check if the view intersects the visible portion of the parent. -        return localRect.intersect(mTempVisibleRect); +        return localRect.intersect(tempVisibleRect);      }      /** @@ -588,7 +613,7 @@ public abstract class ExploreByTouchHelper extends View.AccessibilityDelegate {       *       * @param virtualViewIds The list to populate with visible items       */ -    protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds); +    protected abstract void getVisibleVirtualViews(IntArray virtualViewIds);      /**       * Populates an {@link AccessibilityEvent} with information about the diff --git a/core/res/res/layout/time_header_label.xml b/core/res/res/layout/time_header_label.xml index 84b2b0cdcb88..efb362847df4 100644 --- a/core/res/res/layout/time_header_label.xml +++ b/core/res/res/layout/time_header_label.xml @@ -56,7 +56,9 @@                  android:paddingStart="@dimen/timepicker_ampm_horizontal_padding"                  android:paddingTop="@dimen/timepicker_ampm_vertical_padding"                  android:paddingEnd="@dimen/timepicker_ampm_horizontal_padding" -                android:paddingBottom="@dimen/timepicker_am_bottom_padding" /> +                android:paddingBottom="@dimen/timepicker_am_bottom_padding" +                android:lines="1" +                android:ellipsize="none" />              <CheckedTextView                  android:id="@+id/pm_label"                  android:layout_width="wrap_content" @@ -64,7 +66,9 @@                  android:paddingStart="@dimen/timepicker_ampm_horizontal_padding"                  android:paddingTop="@dimen/timepicker_pm_top_padding"                  android:paddingEnd="@dimen/timepicker_ampm_horizontal_padding" -                android:paddingBottom="@dimen/timepicker_ampm_vertical_padding" /> +                android:paddingBottom="@dimen/timepicker_ampm_vertical_padding" +                android:lines="1" +                android:ellipsize="none" />          </LinearLayout>      </RelativeLayout>  </FrameLayout> |