2022-02-01 13:56:37 -08:00

334 lines
12 KiB
Java

/*
* Copyright (C) 2020 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 com.google.android.material.timepicker;
import com.google.android.material.R;
import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_SINGLE;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.os.Bundle;
import android.os.SystemClock;
import androidx.appcompat.content.res.AppCompatResources;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.TextView;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.timepicker.ClockHandView.OnRotateListener;
import java.util.Arrays;
/**
* A View to display a clock face.
*
* <p>It consists of a {@link ClockHandView} a list of the possible values evenly distributed across
* a circle.
*/
class ClockFaceView extends RadialViewGroup implements OnRotateListener {
private static final float EPSILON = .001f;
private static final int INITIAL_CAPACITY = 12;
private static final String VALUE_PLACEHOLDER = "";
private final ClockHandView clockHandView;
private final Rect textViewRect = new Rect();
private final RectF scratch = new RectF();
private final SparseArray<TextView> textViewPool = new SparseArray<>();
private final AccessibilityDelegateCompat valueAccessibilityDelegate;
private final int[] gradientColors;
private final float[] gradientPositions = new float[] {0f, 0.9f, 1f};
private final int clockHandPadding;
private final int minimumHeight;
private final int minimumWidth;
private final int clockSize;
private String[] values;
private float currentHandRotation;
private final ColorStateList textColor;
public ClockFaceView(@NonNull Context context) {
this(context, null);
}
public ClockFaceView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.materialClockStyle);
}
@SuppressLint("ClickableViewAccessibility")
public ClockFaceView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a =
context.obtainStyledAttributes(
attrs,
R.styleable.ClockFaceView,
defStyleAttr,
R.style.Widget_MaterialComponents_TimePicker_Clock);
Resources res = getResources();
textColor =
MaterialResources.getColorStateList(
context, a, R.styleable.ClockFaceView_clockNumberTextColor);
LayoutInflater.from(context).inflate(R.layout.material_clockface_view, this, true);
clockHandView = findViewById(R.id.material_clock_hand);
clockHandPadding = res.getDimensionPixelSize(R.dimen.material_clock_hand_padding);
int clockHandTextColor =
textColor.getColorForState(
new int[] {android.R.attr.state_selected}, textColor.getDefaultColor());
gradientColors =
new int[] {clockHandTextColor, clockHandTextColor, textColor.getDefaultColor()};
clockHandView.addOnRotateListener(this);
int defaultBackgroundColor = AppCompatResources
.getColorStateList(context, R.color.material_timepicker_clockface)
.getDefaultColor();
ColorStateList backgroundColor =
MaterialResources.getColorStateList(
context, a, R.styleable.ClockFaceView_clockFaceBackgroundColor);
setBackgroundColor(
backgroundColor == null ? defaultBackgroundColor : backgroundColor.getDefaultColor());
getViewTreeObserver()
.addOnPreDrawListener(
new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (!isShown()) {
return true;
}
getViewTreeObserver().removeOnPreDrawListener(this);
int circleRadius =
getHeight() / 2 - clockHandView.getSelectorRadius() - clockHandPadding;
setRadius(circleRadius);
return true;
}
});
setFocusable(true);
a.recycle();
valueAccessibilityDelegate =
new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(
View host, @NonNull AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
int index = (int) host.getTag(R.id.material_value_index);
if (index > 0) {
info.setTraversalAfter(textViewPool.get(index - 1));
}
info.setCollectionItemInfo(
CollectionItemInfoCompat.obtain(
/* rowIndex= */ 0,
/* rowSpan= */ 1,
/* columnIndex =*/ index,
/* columnSpan= */ 1,
/* heading= */ false,
/* selected= */ host.isSelected()));
info.setClickable(true);
info.addAction(AccessibilityActionCompat.ACTION_CLICK);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
long eventTime = SystemClock.uptimeMillis();
float x = host.getX() + host.getWidth() / 2f;
float y = host.getY() + host.getHeight() / 2f;
clockHandView.onTouchEvent(
MotionEvent.obtain(eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0));
clockHandView.onTouchEvent(
MotionEvent.obtain(eventTime, eventTime, MotionEvent.ACTION_UP, x, y, 0));
return true;
}
return super.performAccessibilityAction(host, action, args);
}
};
// Fill clock face with place holders
String[] initialValues = new String[INITIAL_CAPACITY];
Arrays.fill(initialValues, VALUE_PLACEHOLDER);
setValues(initialValues, /* contentDescription= */ 0);
minimumHeight = res.getDimensionPixelSize(R.dimen.material_time_picker_minimum_screen_height);
minimumWidth = res.getDimensionPixelSize(R.dimen.material_time_picker_minimum_screen_width);
clockSize = res.getDimensionPixelSize(R.dimen.material_clock_size);
}
/**
* Sets the list of values that will be shown in the clock face. The first value will be shown in
* the 12 O'Clock position, subsequent values will be evenly distributed after.
*/
public void setValues(String[] values, @StringRes int contentDescription) {
this.values = values;
updateTextViews(contentDescription);
}
private void updateTextViews(@StringRes int contentDescription) {
LayoutInflater inflater = LayoutInflater.from(getContext());
int size = textViewPool.size();
for (int i = 0; i < max(values.length, size); ++i) {
TextView textView = textViewPool.get(i);
if (i >= values.length) {
removeView(textView);
textViewPool.remove(i);
continue;
}
if (textView == null) {
textView = (TextView) inflater.inflate(R.layout.material_clockface_textview, this, false);
textViewPool.put(i, textView);
addView(textView);
}
textView.setVisibility(VISIBLE);
textView.setText(values[i]);
textView.setTag(R.id.material_value_index, i);
ViewCompat.setAccessibilityDelegate(textView, valueAccessibilityDelegate);
textView.setTextColor(textColor);
if (contentDescription != 0) {
Resources res = getResources();
textView.setContentDescription(res.getString(contentDescription, values[i]));
}
}
}
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info);
infoCompat.setCollectionInfo(
CollectionInfoCompat.obtain(
/* rowCount= */ 1,
/* columnCount= */ values.length,
/* hierarchical= */ false,
SELECTION_MODE_SINGLE));
}
@Override
public void setRadius(int radius) {
if (radius != getRadius()) {
super.setRadius(radius);
clockHandView.setCircleRadius(getRadius());
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
findIntersectingTextView();
}
public void setHandRotation(@FloatRange(from = 0f, to = 360f) float rotation) {
clockHandView.setHandRotation(rotation);
findIntersectingTextView();
}
private void findIntersectingTextView() {
RectF selectorBox = clockHandView.getCurrentSelectorBox();
for (int i = 0; i < textViewPool.size(); ++i) {
TextView tv = textViewPool.get(i);
if (tv == null) {
continue;
}
tv.getDrawingRect(textViewRect);
textViewRect.offset(tv.getPaddingLeft(), tv.getPaddingTop());
offsetDescendantRectToMyCoords(tv, textViewRect);
scratch.set(textViewRect);
if (RectF.intersects(selectorBox, scratch)) {
tv.getPaint().setShader(getGradient(selectorBox));
tv.setSelected(true);
} else {
tv.getPaint().setShader(null); // clear
tv.setSelected(false);
}
tv.invalidate();
}
}
private RadialGradient getGradient(RectF selectorBox) {
return new RadialGradient(
(selectorBox.centerX() - scratch.left),
(selectorBox.centerY() - scratch.top),
selectorBox.width() * .5f,
gradientColors,
gradientPositions,
TileMode.CLAMP);
}
@Override
public void onRotate(float rotation, boolean animating) {
if (abs(currentHandRotation - rotation) > EPSILON) {
currentHandRotation = rotation;
findIntersectingTextView();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Resources r = getResources();
DisplayMetrics displayMetrics = r.getDisplayMetrics();
float height = displayMetrics.heightPixels;
float width = displayMetrics.widthPixels;
// If the screen is smaller than our defined values. Scale the clock face
// proportionally to the smaller size
int size = (int) (clockSize / max3(minimumHeight / height, minimumWidth / width, 1f));
int spec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
setMeasuredDimension(size, size);
super.onMeasure(spec, spec);
}
private static float max3(float a, float b, float c) {
return max(max(a, b), c);
}
}