diff options
| author | George Mount <mount@google.com> | 2015-10-27 08:46:44 -0700 |
|---|---|---|
| committer | George Mount <mount@google.com> | 2015-11-06 15:48:52 -0800 |
| commit | e998c3f5f6e7ac21b18cd449088ac2b76b65671f (patch) | |
| tree | 01bc6c19bdfa93d7767f2aa91c991c9a9d113a12 /core/java/android/widget/SimpleMonthView.java | |
| parent | c406898e8c8cd8003b4dbc55596f313db278e8ee (diff) | |
Improve DatePicker focus for keyboards.
Bug 24873983
Focus moves properly within the SimpleMonthView and between
components of the DatePicker.
Change-Id: I61778f9b4f19536ff6c7a512f9b4faf7bf4447a7
Diffstat (limited to 'core/java/android/widget/SimpleMonthView.java')
| -rw-r--r-- | core/java/android/widget/SimpleMonthView.java | 253 |
1 files changed, 247 insertions, 6 deletions
diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java index e9325efcea09..6edce91db504 100644 --- a/core/java/android/widget/SimpleMonthView.java +++ b/core/java/android/widget/SimpleMonthView.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -34,8 +35,10 @@ import android.util.AttributeSet; import android.util.IntArray; import android.util.MathUtils; import android.util.StateSet; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; @@ -61,11 +64,14 @@ class SimpleMonthView extends View { private static final String DEFAULT_TITLE_FORMAT = "MMMMy"; private static final String DAY_OF_WEEK_FORMAT = "EEEEE"; + private static final int SELECTED_HIGHLIGHT_ALPHA = 0xB0; + private final TextPaint mMonthPaint = new TextPaint(); private final TextPaint mDayOfWeekPaint = new TextPaint(); private final TextPaint mDayPaint = new TextPaint(); private final Paint mDaySelectorPaint = new Paint(); private final Paint mDayHighlightPaint = new Paint(); + private final Paint mDayHighlightSelectorPaint = new Paint(); private final Calendar mCalendar = Calendar.getInstance(); private final Calendar mDayOfWeekLabelCalendar = Calendar.getInstance(); @@ -130,7 +136,9 @@ class SimpleMonthView extends View { private ColorStateList mDayTextColor; - private int mTouchedItem = -1; + private int mHighlightedDay = -1; + private int mPreviouslyHighlightedDay = -1; + private boolean mIsTouchHighlighted = false; public SimpleMonthView(Context context) { this(context, null); @@ -268,6 +276,9 @@ class SimpleMonthView extends View { mDayHighlightPaint.setAntiAlias(true); mDayHighlightPaint.setStyle(Style.FILL); + mDayHighlightSelectorPaint.setAntiAlias(true); + mDayHighlightSelectorPaint.setStyle(Style.FILL); + mDayPaint.setAntiAlias(true); mDayPaint.setTextSize(dayTextSize); mDayPaint.setTypeface(Typeface.create(dayTypeface, 0)); @@ -296,6 +307,8 @@ class SimpleMonthView extends View { final int activatedColor = dayBackgroundColor.getColorForState( StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0); mDaySelectorPaint.setColor(activatedColor); + mDayHighlightSelectorPaint.setColor(activatedColor); + mDayHighlightSelectorPaint.setAlpha(SELECTED_HIGHLIGHT_ALPHA); invalidate(); } @@ -326,8 +339,10 @@ class SimpleMonthView extends View { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: final int touchedItem = getDayAtLocation(x, y); - if (mTouchedItem != touchedItem) { - mTouchedItem = touchedItem; + mIsTouchHighlighted = true; + if (mHighlightedDay != touchedItem) { + mHighlightedDay = touchedItem; + mPreviouslyHighlightedDay = touchedItem; invalidate(); } if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) { @@ -342,7 +357,8 @@ class SimpleMonthView extends View { // Fall through. case MotionEvent.ACTION_CANCEL: // Reset touched day on stream end. - mTouchedItem = -1; + mHighlightedDay = -1; + mIsTouchHighlighted = false; invalidate(); break; } @@ -350,6 +366,228 @@ class SimpleMonthView extends View { } @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // We need to handle focus change within the SimpleMonthView because we are simulating + // multiple Views. The arrow keys will move between days until there is no space (no + // day to the left, top, right, or bottom). Focus forward and back jumps out of the + // SimpleMonthView, skipping over other SimpleMonthViews in the parent ViewPager + // to the next focusable View in the hierarchy. + boolean focusChanged = false; + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (event.hasNoModifiers()) { + focusChanged = moveOneDay(isLayoutRtl()); + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (event.hasNoModifiers()) { + focusChanged = moveOneDay(!isLayoutRtl()); + } + break; + case KeyEvent.KEYCODE_DPAD_UP: + if (event.hasNoModifiers()) { + ensureFocusedDay(); + if (mHighlightedDay > 7) { + mHighlightedDay -= 7; + focusChanged = true; + } + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (event.hasNoModifiers()) { + ensureFocusedDay(); + if (mHighlightedDay <= mDaysInMonth - 7) { + mHighlightedDay += 7; + focusChanged = true; + } + } + break; + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (mHighlightedDay != -1) { + onDayClicked(mHighlightedDay); + return true; + } + break; + case KeyEvent.KEYCODE_TAB: { + int focusChangeDirection = 0; + if (event.hasNoModifiers()) { + focusChangeDirection = View.FOCUS_FORWARD; + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + focusChangeDirection = View.FOCUS_BACKWARD; + } + if (focusChangeDirection != 0) { + final ViewParent parent = getParent(); + // move out of the ViewPager next/previous + View nextFocus = this; + do { + nextFocus = nextFocus.focusSearch(focusChangeDirection); + } while (nextFocus != null && nextFocus != this && + nextFocus.getParent() == parent); + if (nextFocus != null) { + nextFocus.requestFocus(); + return true; + } + } + break; + } + } + if (focusChanged) { + invalidate(); + return true; + } else { + return super.onKeyDown(keyCode, event); + } + } + + private boolean moveOneDay(boolean positive) { + ensureFocusedDay(); + boolean focusChanged = false; + if (positive) { + if (!isLastDayOfWeek(mHighlightedDay) && mHighlightedDay < mDaysInMonth) { + mHighlightedDay++; + focusChanged = true; + } + } else { + if (!isFirstDayOfWeek(mHighlightedDay) && mHighlightedDay > 1) { + mHighlightedDay--; + focusChanged = true; + } + } + return focusChanged; + } + + @Override + protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction, + @Nullable Rect previouslyFocusedRect) { + if (gainFocus) { + // If we've gained focus through arrow keys, we should find the day closest + // to the focus rect. If we've gained focus through forward/back, we should + // focus on the selected day if there is one. + final int offset = findDayOffset(); + switch(direction) { + case View.FOCUS_RIGHT: { + int row = findClosestRow(previouslyFocusedRect); + mHighlightedDay = row == 0 ? 1 : (row * DAYS_IN_WEEK) - offset + 1; + break; + } + case View.FOCUS_LEFT: { + int row = findClosestRow(previouslyFocusedRect) + 1; + mHighlightedDay = Math.min(mDaysInMonth, (row * DAYS_IN_WEEK) - offset); + break; + } + case View.FOCUS_DOWN: { + final int col = findClosestColumn(previouslyFocusedRect); + final int day = col - offset + 1; + mHighlightedDay = day < 1 ? day + DAYS_IN_WEEK : day; + break; + } + case View.FOCUS_UP: { + final int col = findClosestColumn(previouslyFocusedRect); + final int maxWeeks = (offset + mDaysInMonth) / DAYS_IN_WEEK; + final int day = col - offset + (DAYS_IN_WEEK * maxWeeks) + 1; + mHighlightedDay = day > mDaysInMonth ? day - DAYS_IN_WEEK : day; + break; + } + } + ensureFocusedDay(); + invalidate(); + } + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + } + + /** + * Returns the row (0 indexed) closest to previouslyFocusedRect or center if null. + */ + private int findClosestRow(@Nullable Rect previouslyFocusedRect) { + if (previouslyFocusedRect == null) { + return 3; + } else { + int centerY = previouslyFocusedRect.centerY(); + + final TextPaint p = mDayPaint; + final int headerHeight = mMonthHeight + mDayOfWeekHeight; + final int rowHeight = mDayHeight; + + // Text is vertically centered within the row height. + final float halfLineHeight = (p.ascent() + p.descent()) / 2f; + final int rowCenter = headerHeight + rowHeight / 2; + + centerY -= rowCenter - halfLineHeight; + int row = Math.round(centerY / (float) rowHeight); + final int maxDay = findDayOffset() + mDaysInMonth; + final int maxRows = (maxDay / DAYS_IN_WEEK) - ((maxDay % DAYS_IN_WEEK == 0) ? 1 : 0); + + row = MathUtils.constrain(row, 0, maxRows); + return row; + } + } + + /** + * Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null. + * The 0 index is related to the first day of the week. + */ + private int findClosestColumn(@Nullable Rect previouslyFocusedRect) { + if (previouslyFocusedRect == null) { + return DAYS_IN_WEEK / 2; + } else { + int centerX = previouslyFocusedRect.centerX() - mPaddingLeft; + final int columnFromLeft = + MathUtils.constrain(centerX / mCellWidth, 0, DAYS_IN_WEEK - 1); + return isLayoutRtl() ? DAYS_IN_WEEK - columnFromLeft - 1: columnFromLeft; + } + } + + @Override + public void getFocusedRect(Rect r) { + if (mHighlightedDay > 0) { + getBoundsForDay(mHighlightedDay, r); + } else { + super.getFocusedRect(r); + } + } + + @Override + protected void onFocusLost() { + if (!mIsTouchHighlighted) { + // Unhighlight a day. + mPreviouslyHighlightedDay = mHighlightedDay; + mHighlightedDay = -1; + invalidate(); + } + super.onFocusLost(); + } + + /** + * Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day, + * if possible, or the first day of the month if not. + */ + private void ensureFocusedDay() { + if (mHighlightedDay != -1) { + return; + } + if (mPreviouslyHighlightedDay != -1) { + mHighlightedDay = mPreviouslyHighlightedDay; + return; + } + if (mActivatedDay != -1) { + mHighlightedDay = mActivatedDay; + return; + } + mHighlightedDay = 1; + } + + private boolean isFirstDayOfWeek(int day) { + final int offset = findDayOffset(); + return (offset + day - 1) % DAYS_IN_WEEK == 0; + } + + private boolean isLastDayOfWeek(int day) { + final int offset = findDayOffset(); + return (offset + day) % DAYS_IN_WEEK == 0; + } + + @Override protected void onDraw(Canvas canvas) { final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); @@ -432,12 +670,15 @@ class SimpleMonthView extends View { } final boolean isDayActivated = mActivatedDay == day; + final boolean isDayHighlighted = mHighlightedDay == day; if (isDayActivated) { stateMask |= StateSet.VIEW_STATE_ACTIVATED; // Adjust the circle to be centered on the row. - canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, mDaySelectorPaint); - } else if (mTouchedItem == day) { + final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint : + mDaySelectorPaint; + canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint); + } else if (isDayHighlighted) { stateMask |= StateSet.VIEW_STATE_PRESSED; if (isDayEnabled) { |
