Merge "Add support for Explore by Touch to RadialTimePickerView" into lmp-mr1-dev

automerge: 1ac5266

* commit '1ac52667ee03af090bb13d1ccf45ee6f38ddee3c':
  Add support for Explore by Touch to RadialTimePickerView
This commit is contained in:
Alan Viverette
2014-10-24 20:48:05 +00:00
committed by android-build-merger
6 changed files with 648 additions and 212 deletions

View File

@@ -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;
}
}

View File

@@ -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,104 +1262,13 @@ 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);
}
/**
* 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;
}
int changeMultiplier = 0;
if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
changeMultiplier = 1;
} else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
changeMultiplier = -1;
}
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();
}
int degrees = value * stepSize;
degrees = snapOnly30s(degrees, changeMultiplier);
value = degrees / stepSize;
final int maxValue;
int minValue = 0;
if (mShowHours) {
if (mIs24HourMode) {
maxValue = 23;
} else {
maxValue = 12;
minValue = 1;
}
} else {
maxValue = 55;
}
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;
}
if (mShowHours) {
setCurrentHour(value);
if (mListener != null) {
mListener.onValueSelected(HOURS, value, false);
}
} else {
setCurrentMinute(value);
if (mListener != null) {
mListener.onValueSelected(MINUTES, value, false);
}
}
return true;
}
return false;
return super.dispatchHoverEvent(event);
}
public void setInputEnabled(boolean inputEnabled) {
@@ -1281,6 +1276,265 @@ public class RadialTimePickerView extends View implements View.OnTouchListener {
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);
}
@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);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
if (super.performAccessibilityAction(host, action, arguments)) {
return true;
}
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;
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;
}
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) {
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 {
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));
}
}
}
}
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
event.setClassName(getClass().getName());
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 {
private int mValue;

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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;
/** View's context **/
private Context mContext;
/** Lazily-created temporary data structure used to compute visibility. */
private Rect mTempVisibleRect;
/** 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

View File

@@ -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>