6.6.3 - Alpha2 - 本地化 Material Date Time Picker (版本 4.2.3)
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.wdullaer.materialdatetimepicker.time;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.Paint.Align;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.R;
|
||||
import com.wdullaer.materialdatetimepicker.Utils;
|
||||
|
||||
import java.text.DateFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Draw the two smaller AM and PM circles next to where the larger circle will be.
|
||||
*/
|
||||
public class AmPmCirclesView extends View {
|
||||
private static final String TAG = "AmPmCirclesView";
|
||||
|
||||
// Alpha level for selected circle.
|
||||
private static final int SELECTED_ALPHA = Utils.SELECTED_ALPHA;
|
||||
private static final int SELECTED_ALPHA_THEME_DARK = Utils.SELECTED_ALPHA_THEME_DARK;
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
private int mSelectedAlpha;
|
||||
private int mTouchedColor;
|
||||
private int mUnselectedColor;
|
||||
private int mAmPmTextColor;
|
||||
private int mAmPmSelectedTextColor;
|
||||
private int mAmPmDisabledTextColor;
|
||||
private int mSelectedColor;
|
||||
private float mCircleRadiusMultiplier;
|
||||
private float mAmPmCircleRadiusMultiplier;
|
||||
private String mAmText;
|
||||
private String mPmText;
|
||||
private boolean mAmDisabled;
|
||||
private boolean mPmDisabled;
|
||||
private boolean mIsInitialized;
|
||||
|
||||
private static final int AM = TimePickerDialog.AM;
|
||||
private static final int PM = TimePickerDialog.PM;
|
||||
|
||||
private boolean mDrawValuesReady;
|
||||
private int mAmPmCircleRadius;
|
||||
private int mAmXCenter;
|
||||
private int mPmXCenter;
|
||||
private int mAmPmYCenter;
|
||||
private int mAmOrPm;
|
||||
private int mAmOrPmPressed;
|
||||
|
||||
public AmPmCirclesView(Context context) {
|
||||
super(context);
|
||||
mIsInitialized = false;
|
||||
}
|
||||
|
||||
public void initialize(Context context, Locale locale, TimePickerController controller, int amOrPm) {
|
||||
if (mIsInitialized) {
|
||||
Log.e(TAG, "AmPmCirclesView may only be initialized once.");
|
||||
return;
|
||||
}
|
||||
|
||||
Resources res = context.getResources();
|
||||
|
||||
if (controller.isThemeDark()) {
|
||||
mUnselectedColor = ContextCompat.getColor(context, R.color.mdtp_circle_background_dark_theme);
|
||||
mAmPmTextColor = ContextCompat.getColor(context, R.color.mdtp_white);
|
||||
mAmPmDisabledTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_disabled_dark_theme);
|
||||
mSelectedAlpha = SELECTED_ALPHA_THEME_DARK;
|
||||
} else {
|
||||
mUnselectedColor = ContextCompat.getColor(context, R.color.mdtp_white);
|
||||
mAmPmTextColor = ContextCompat.getColor(context, R.color.mdtp_ampm_text_color);
|
||||
mAmPmDisabledTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_disabled);
|
||||
mSelectedAlpha = SELECTED_ALPHA;
|
||||
}
|
||||
|
||||
mSelectedColor = controller.getAccentColor();
|
||||
mTouchedColor = Utils.darkenColor(mSelectedColor);
|
||||
mAmPmSelectedTextColor = ContextCompat.getColor(context, R.color.mdtp_white);
|
||||
|
||||
String typefaceFamily = res.getString(R.string.mdtp_sans_serif);
|
||||
Typeface tf = Typeface.create(typefaceFamily, Typeface.NORMAL);
|
||||
mPaint.setTypeface(tf);
|
||||
mPaint.setAntiAlias(true);
|
||||
mPaint.setTextAlign(Align.CENTER);
|
||||
|
||||
mCircleRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_circle_radius_multiplier));
|
||||
mAmPmCircleRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier));
|
||||
String[] amPmTexts = new DateFormatSymbols(locale).getAmPmStrings();
|
||||
mAmText = amPmTexts[0];
|
||||
mPmText = amPmTexts[1];
|
||||
|
||||
mAmDisabled = controller.isAmDisabled();
|
||||
mPmDisabled = controller.isPmDisabled();
|
||||
|
||||
setAmOrPm(amOrPm);
|
||||
mAmOrPmPressed = -1;
|
||||
|
||||
mIsInitialized = true;
|
||||
}
|
||||
|
||||
public void setAmOrPm(int amOrPm) {
|
||||
mAmOrPm = amOrPm;
|
||||
}
|
||||
|
||||
public void setAmOrPmPressed(int amOrPmPressed) {
|
||||
mAmOrPmPressed = amOrPmPressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate whether the coordinates are touching the AM or PM circle.
|
||||
*/
|
||||
public int getIsTouchingAmOrPm(float xCoord, float yCoord) {
|
||||
if (!mDrawValuesReady) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int squaredYDistance = (int) ((yCoord - mAmPmYCenter)*(yCoord - mAmPmYCenter));
|
||||
|
||||
int distanceToAmCenter =
|
||||
(int) Math.sqrt((xCoord - mAmXCenter)*(xCoord - mAmXCenter) + squaredYDistance);
|
||||
if (distanceToAmCenter <= mAmPmCircleRadius && !mAmDisabled) {
|
||||
return AM;
|
||||
}
|
||||
|
||||
int distanceToPmCenter =
|
||||
(int) Math.sqrt((xCoord - mPmXCenter)*(xCoord - mPmXCenter) + squaredYDistance);
|
||||
if (distanceToPmCenter <= mAmPmCircleRadius && !mPmDisabled) {
|
||||
return PM;
|
||||
}
|
||||
|
||||
// Neither was close enough.
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
int viewWidth = getWidth();
|
||||
if (viewWidth == 0 || !mIsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mDrawValuesReady) {
|
||||
int layoutXCenter = getWidth() / 2;
|
||||
int layoutYCenter = getHeight() / 2;
|
||||
int circleRadius =
|
||||
(int) (Math.min(layoutXCenter, layoutYCenter) * mCircleRadiusMultiplier);
|
||||
mAmPmCircleRadius = (int) (circleRadius * mAmPmCircleRadiusMultiplier);
|
||||
layoutYCenter += mAmPmCircleRadius*0.75;
|
||||
int textSize = mAmPmCircleRadius * 3 / 4;
|
||||
mPaint.setTextSize(textSize);
|
||||
|
||||
// Line up the vertical center of the AM/PM circles with the bottom of the main circle.
|
||||
mAmPmYCenter = layoutYCenter - mAmPmCircleRadius / 2 + circleRadius;
|
||||
// Line up the horizontal edges of the AM/PM circles with the horizontal edges
|
||||
// of the main circle.
|
||||
mAmXCenter = layoutXCenter - circleRadius + mAmPmCircleRadius;
|
||||
mPmXCenter = layoutXCenter + circleRadius - mAmPmCircleRadius;
|
||||
|
||||
mDrawValuesReady = true;
|
||||
}
|
||||
|
||||
// We'll need to draw either a lighter blue (for selection), a darker blue (for touching)
|
||||
// or white (for not selected).
|
||||
int amColor = mUnselectedColor;
|
||||
int amAlpha = 255;
|
||||
int amTextColor = mAmPmTextColor;
|
||||
int pmColor = mUnselectedColor;
|
||||
int pmAlpha = 255;
|
||||
int pmTextColor = mAmPmTextColor;
|
||||
|
||||
if (mAmOrPm == AM) {
|
||||
amColor = mSelectedColor;
|
||||
amAlpha = mSelectedAlpha;
|
||||
amTextColor = mAmPmSelectedTextColor;
|
||||
} else if (mAmOrPm == PM) {
|
||||
pmColor = mSelectedColor;
|
||||
pmAlpha = mSelectedAlpha;
|
||||
pmTextColor = mAmPmSelectedTextColor;
|
||||
}
|
||||
if (mAmOrPmPressed == AM) {
|
||||
amColor = mTouchedColor;
|
||||
amAlpha = mSelectedAlpha;
|
||||
} else if (mAmOrPmPressed == PM) {
|
||||
pmColor = mTouchedColor;
|
||||
pmAlpha = mSelectedAlpha;
|
||||
}
|
||||
if (mAmDisabled) {
|
||||
amColor = mUnselectedColor;
|
||||
amTextColor = mAmPmDisabledTextColor;
|
||||
}
|
||||
if (mPmDisabled) {
|
||||
pmColor = mUnselectedColor;
|
||||
pmTextColor = mAmPmDisabledTextColor;
|
||||
}
|
||||
|
||||
// Draw the two circles.
|
||||
mPaint.setColor(amColor);
|
||||
mPaint.setAlpha(amAlpha);
|
||||
canvas.drawCircle(mAmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint);
|
||||
mPaint.setColor(pmColor);
|
||||
mPaint.setAlpha(pmAlpha);
|
||||
canvas.drawCircle(mPmXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaint);
|
||||
|
||||
// Draw the AM/PM texts on top.
|
||||
mPaint.setColor(amTextColor);
|
||||
int textYCenter = mAmPmYCenter - (int) (mPaint.descent() + mPaint.ascent()) / 2;
|
||||
canvas.drawText(mAmText, mAmXCenter, textYCenter, mPaint);
|
||||
mPaint.setColor(pmTextColor);
|
||||
canvas.drawText(mPmText, mPmXCenter, textYCenter, mPaint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.wdullaer.materialdatetimepicker.time;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.R;
|
||||
|
||||
/**
|
||||
* Draws a simple white circle on which the numbers will be drawn.
|
||||
*/
|
||||
public class CircleView extends View {
|
||||
private static final String TAG = "CircleView";
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
private boolean mIs24HourMode;
|
||||
private int mCircleColor;
|
||||
private int mDotColor;
|
||||
private float mCircleRadiusMultiplier;
|
||||
private float mAmPmCircleRadiusMultiplier;
|
||||
private boolean mIsInitialized;
|
||||
|
||||
private boolean mDrawValuesReady;
|
||||
private int mXCenter;
|
||||
private int mYCenter;
|
||||
private int mCircleRadius;
|
||||
|
||||
public CircleView(Context context) {
|
||||
super(context);
|
||||
|
||||
mIsInitialized = false;
|
||||
}
|
||||
|
||||
public void initialize(Context context, TimePickerController controller) {
|
||||
if (mIsInitialized) {
|
||||
Log.e(TAG, "CircleView may only be initialized once.");
|
||||
return;
|
||||
}
|
||||
|
||||
Resources res = context.getResources();
|
||||
|
||||
int colorRes = controller.isThemeDark() ? R.color.mdtp_circle_background_dark_theme : R.color.mdtp_circle_color;
|
||||
mCircleColor = ContextCompat.getColor(context, colorRes);
|
||||
mDotColor = controller.getAccentColor();
|
||||
mPaint.setAntiAlias(true);
|
||||
|
||||
mIs24HourMode = controller.is24HourMode();
|
||||
if (mIs24HourMode || controller.getVersion() != TimePickerDialog.Version.VERSION_1) {
|
||||
mCircleRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_circle_radius_multiplier_24HourMode));
|
||||
} else {
|
||||
mCircleRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_circle_radius_multiplier));
|
||||
mAmPmCircleRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier));
|
||||
}
|
||||
|
||||
mIsInitialized = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
int viewWidth = getWidth();
|
||||
if (viewWidth == 0 || !mIsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mDrawValuesReady) {
|
||||
mXCenter = getWidth() / 2;
|
||||
mYCenter = getHeight() / 2;
|
||||
mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier);
|
||||
|
||||
if (!mIs24HourMode) {
|
||||
// We'll need to draw the AM/PM circles, so the main circle will need to have
|
||||
// a slightly higher center. To keep the entire view centered vertically, we'll
|
||||
// have to push it up by half the radius of the AM/PM circles.
|
||||
int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier);
|
||||
mYCenter -= amPmCircleRadius*0.75;
|
||||
}
|
||||
|
||||
mDrawValuesReady = true;
|
||||
}
|
||||
|
||||
// Draw the white circle.
|
||||
mPaint.setColor(mCircleColor);
|
||||
canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaint);
|
||||
|
||||
// Draw a small black circle in the center.
|
||||
mPaint.setColor(mDotColor);
|
||||
canvas.drawCircle(mXCenter, mYCenter, 8, mPaint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package com.wdullaer.materialdatetimepicker.time;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import static com.wdullaer.materialdatetimepicker.time.TimePickerDialog.HOUR_INDEX;
|
||||
import static com.wdullaer.materialdatetimepicker.time.TimePickerDialog.MINUTE_INDEX;
|
||||
|
||||
/**
|
||||
* An implementation of TimepointLimiter which implements the most common ways to restrict Timepoints
|
||||
* in a TimePickerDialog
|
||||
* Created by wdullaer on 20/06/17.
|
||||
*/
|
||||
|
||||
class DefaultTimepointLimiter implements TimepointLimiter {
|
||||
private TreeSet<Timepoint> mSelectableTimes = new TreeSet<>();
|
||||
private TreeSet<Timepoint> mDisabledTimes = new TreeSet<>();
|
||||
private TreeSet<Timepoint> exclusiveSelectableTimes = new TreeSet<>();
|
||||
private Timepoint mMinTime;
|
||||
private Timepoint mMaxTime;
|
||||
|
||||
DefaultTimepointLimiter() {}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public DefaultTimepointLimiter(Parcel in) {
|
||||
mMinTime = in.readParcelable(Timepoint.class.getClassLoader());
|
||||
mMaxTime = in.readParcelable(Timepoint.class.getClassLoader());
|
||||
mSelectableTimes.addAll(Arrays.asList(in.createTypedArray(Timepoint.CREATOR)));
|
||||
mDisabledTimes.addAll(Arrays.asList(in.createTypedArray(Timepoint.CREATOR)));
|
||||
exclusiveSelectableTimes = getExclusiveSelectableTimes(mSelectableTimes, mDisabledTimes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeParcelable(mMinTime, flags);
|
||||
out.writeParcelable(mMaxTime, flags);
|
||||
out.writeTypedArray(mSelectableTimes.toArray(new Timepoint[mSelectableTimes.size()]), flags);
|
||||
out.writeTypedArray(mDisabledTimes.toArray(new Timepoint[mDisabledTimes.size()]), flags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static final Parcelable.Creator<DefaultTimepointLimiter> CREATOR
|
||||
= new Parcelable.Creator<DefaultTimepointLimiter>() {
|
||||
public DefaultTimepointLimiter createFromParcel(Parcel in) {
|
||||
return new DefaultTimepointLimiter(in);
|
||||
}
|
||||
|
||||
public DefaultTimepointLimiter[] newArray(int size) {
|
||||
return new DefaultTimepointLimiter[size];
|
||||
}
|
||||
};
|
||||
|
||||
void setMinTime(@NonNull Timepoint minTime) {
|
||||
if(mMaxTime != null && minTime.compareTo(mMaxTime) > 0)
|
||||
throw new IllegalArgumentException("Minimum time must be smaller than the maximum time");
|
||||
mMinTime = minTime;
|
||||
}
|
||||
|
||||
void setMaxTime(@NonNull Timepoint maxTime) {
|
||||
if(mMinTime != null && maxTime.compareTo(mMinTime) < 0)
|
||||
throw new IllegalArgumentException("Maximum time must be greater than the minimum time");
|
||||
mMaxTime = maxTime;
|
||||
}
|
||||
|
||||
void setSelectableTimes(@NonNull Timepoint[] selectableTimes) {
|
||||
mSelectableTimes.addAll(Arrays.asList(selectableTimes));
|
||||
exclusiveSelectableTimes = getExclusiveSelectableTimes(mSelectableTimes, mDisabledTimes);
|
||||
}
|
||||
|
||||
void setDisabledTimes(@NonNull Timepoint[] disabledTimes) {
|
||||
mDisabledTimes.addAll(Arrays.asList(disabledTimes));
|
||||
exclusiveSelectableTimes = getExclusiveSelectableTimes(mSelectableTimes, mDisabledTimes);
|
||||
}
|
||||
|
||||
@Nullable Timepoint getMinTime() {
|
||||
return mMinTime;
|
||||
}
|
||||
|
||||
@Nullable Timepoint getMaxTime() {
|
||||
return mMaxTime;
|
||||
}
|
||||
|
||||
@NonNull Timepoint[] getSelectableTimes() {
|
||||
return mSelectableTimes.toArray(new Timepoint[mSelectableTimes.size()]);
|
||||
}
|
||||
|
||||
@NonNull Timepoint[] getDisabledTimes() {
|
||||
return mDisabledTimes.toArray(new Timepoint[mDisabledTimes.size()]);
|
||||
}
|
||||
|
||||
@NonNull private TreeSet<Timepoint> getExclusiveSelectableTimes(@NonNull TreeSet<Timepoint> selectable, @NonNull TreeSet<Timepoint> disabled) {
|
||||
TreeSet<Timepoint> output = new TreeSet<>(selectable);
|
||||
output.removeAll(disabled);
|
||||
return output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOutOfRange(@Nullable Timepoint current, int index, @NonNull Timepoint.TYPE resolution) {
|
||||
if (current == null) return false;
|
||||
|
||||
if (index == HOUR_INDEX) {
|
||||
if (mMinTime != null && mMinTime.getHour() > current.getHour()) return true;
|
||||
|
||||
if (mMaxTime != null && mMaxTime.getHour()+1 <= current.getHour()) return true;
|
||||
|
||||
if (!exclusiveSelectableTimes.isEmpty()) {
|
||||
Timepoint ceil = exclusiveSelectableTimes.ceiling(current);
|
||||
Timepoint floor = exclusiveSelectableTimes.floor(current);
|
||||
return !(current.equals(ceil, Timepoint.TYPE.HOUR) || current.equals(floor, Timepoint.TYPE.HOUR));
|
||||
}
|
||||
|
||||
if (!mDisabledTimes.isEmpty() && resolution == Timepoint.TYPE.HOUR) {
|
||||
Timepoint ceil = mDisabledTimes.ceiling(current);
|
||||
Timepoint floor = mDisabledTimes.floor(current);
|
||||
return current.equals(ceil, Timepoint.TYPE.HOUR) || current.equals(floor, Timepoint.TYPE.HOUR);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
else if (index == MINUTE_INDEX) {
|
||||
if (mMinTime != null) {
|
||||
Timepoint roundedMin = new Timepoint(mMinTime.getHour(), mMinTime.getMinute());
|
||||
if (roundedMin.compareTo(current) > 0) return true;
|
||||
}
|
||||
|
||||
if (mMaxTime != null) {
|
||||
Timepoint roundedMax = new Timepoint(mMaxTime.getHour(), mMaxTime.getMinute(), 59);
|
||||
if (roundedMax.compareTo(current) < 0) return true;
|
||||
}
|
||||
|
||||
if (!exclusiveSelectableTimes.isEmpty()) {
|
||||
Timepoint ceil = exclusiveSelectableTimes.ceiling(current);
|
||||
Timepoint floor = exclusiveSelectableTimes.floor(current);
|
||||
return !(current.equals(ceil, Timepoint.TYPE.MINUTE) || current.equals(floor, Timepoint.TYPE.MINUTE));
|
||||
}
|
||||
|
||||
if (!mDisabledTimes.isEmpty() && resolution == Timepoint.TYPE.MINUTE) {
|
||||
Timepoint ceil = mDisabledTimes.ceiling(current);
|
||||
Timepoint floor = mDisabledTimes.floor(current);
|
||||
boolean ceilExclude = current.equals(ceil, Timepoint.TYPE.MINUTE);
|
||||
boolean floorExclude = current.equals(floor, Timepoint.TYPE.MINUTE);
|
||||
return ceilExclude || floorExclude;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
else return isOutOfRange(current);
|
||||
}
|
||||
|
||||
public boolean isOutOfRange(@NonNull Timepoint current) {
|
||||
if (mMinTime != null && mMinTime.compareTo(current) > 0) return true;
|
||||
|
||||
if (mMaxTime != null && mMaxTime.compareTo(current) < 0) return true;
|
||||
|
||||
if (!exclusiveSelectableTimes.isEmpty()) return !exclusiveSelectableTimes.contains(current);
|
||||
|
||||
return mDisabledTimes.contains(current);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SimplifiableIfStatement")
|
||||
@Override
|
||||
public boolean isAmDisabled() {
|
||||
Timepoint midday = new Timepoint(12);
|
||||
|
||||
if (mMinTime != null && mMinTime.compareTo(midday) >= 0) return true;
|
||||
|
||||
if (!exclusiveSelectableTimes.isEmpty()) return exclusiveSelectableTimes.first().compareTo(midday) >= 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("SimplifiableIfStatement")
|
||||
@Override
|
||||
public boolean isPmDisabled() {
|
||||
Timepoint midday = new Timepoint(12);
|
||||
|
||||
if (mMaxTime != null && mMaxTime.compareTo(midday) < 0) return true;
|
||||
|
||||
if (!exclusiveSelectableTimes.isEmpty()) return exclusiveSelectableTimes.last().compareTo(midday) < 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Timepoint roundToNearest(@NonNull Timepoint time,@Nullable Timepoint.TYPE type, @NonNull Timepoint.TYPE resolution) {
|
||||
if (mMinTime != null && mMinTime.compareTo(time) > 0) return mMinTime;
|
||||
|
||||
if (mMaxTime != null && mMaxTime.compareTo(time) < 0) return mMaxTime;
|
||||
|
||||
// type == SECOND: cannot change anything, return input
|
||||
if (type == Timepoint.TYPE.SECOND) return time;
|
||||
|
||||
if (!exclusiveSelectableTimes.isEmpty()) {
|
||||
Timepoint floor = exclusiveSelectableTimes.floor(time);
|
||||
Timepoint ceil = exclusiveSelectableTimes.ceiling(time);
|
||||
|
||||
if (floor == null || ceil == null) {
|
||||
Timepoint t = floor == null ? ceil : floor;
|
||||
if (type == null) return t;
|
||||
if (t.getHour() != time.getHour()) return time;
|
||||
if (type == Timepoint.TYPE.MINUTE && t.getMinute() != time.getMinute()) return time;
|
||||
return t;
|
||||
}
|
||||
|
||||
if (type == Timepoint.TYPE.HOUR) {
|
||||
if (floor.getHour() != time.getHour() && ceil.getHour() == time.getHour()) return ceil;
|
||||
if (floor.getHour() == time.getHour() && ceil.getHour() != time.getHour()) return floor;
|
||||
if (floor.getHour() != time.getHour() && ceil.getHour() != time.getHour()) return time;
|
||||
}
|
||||
|
||||
if (type == Timepoint.TYPE.MINUTE) {
|
||||
if (floor.getHour() != time.getHour() && ceil.getHour() != time.getHour()) return time;
|
||||
if (floor.getHour() != time.getHour() && ceil.getHour() == time.getHour()) {
|
||||
return ceil.getMinute() == time.getMinute() ? ceil : time;
|
||||
}
|
||||
if (floor.getHour() == time.getHour() && ceil.getHour() != time.getHour()) {
|
||||
return floor.getMinute() == time.getMinute() ? floor : time;
|
||||
}
|
||||
if (floor.getMinute() != time.getMinute() && ceil.getMinute() == time.getMinute()) return ceil;
|
||||
if (floor.getMinute() == time.getMinute() && ceil.getMinute() != time.getMinute()) return floor;
|
||||
if (floor.getMinute() != time.getMinute() && ceil.getMinute() != time.getMinute()) return time;
|
||||
}
|
||||
|
||||
int floorDist = Math.abs(time.compareTo(floor));
|
||||
int ceilDist = Math.abs(time.compareTo(ceil));
|
||||
|
||||
return floorDist < ceilDist ? floor : ceil;
|
||||
}
|
||||
|
||||
if (!mDisabledTimes.isEmpty()) {
|
||||
// if type matches resolution: cannot change anything, return input
|
||||
if (type != null && type == resolution) return time;
|
||||
|
||||
if (resolution == Timepoint.TYPE.SECOND) {
|
||||
if (!mDisabledTimes.contains(time)) return time;
|
||||
return searchValidTimePoint(time, type, resolution);
|
||||
}
|
||||
|
||||
if (resolution == Timepoint.TYPE.MINUTE) {
|
||||
Timepoint ceil = mDisabledTimes.ceiling(time);
|
||||
Timepoint floor = mDisabledTimes.floor(time);
|
||||
boolean ceilDisabled = time.equals(ceil, Timepoint.TYPE.MINUTE);
|
||||
boolean floorDisabled = time.equals(floor, Timepoint.TYPE.MINUTE);
|
||||
|
||||
if (ceilDisabled || floorDisabled) return searchValidTimePoint(time, type, resolution);
|
||||
return time;
|
||||
}
|
||||
|
||||
if (resolution == Timepoint.TYPE.HOUR) {
|
||||
Timepoint ceil = mDisabledTimes.ceiling(time);
|
||||
Timepoint floor = mDisabledTimes.floor(time);
|
||||
boolean ceilDisabled = time.equals(ceil, Timepoint.TYPE.HOUR);
|
||||
boolean floorDisabled = time.equals(floor, Timepoint.TYPE.HOUR);
|
||||
|
||||
if (ceilDisabled || floorDisabled) return searchValidTimePoint(time, type, resolution);
|
||||
return time;
|
||||
}
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
private Timepoint searchValidTimePoint(@NonNull Timepoint time, @Nullable Timepoint.TYPE type, @NonNull Timepoint.TYPE resolution) {
|
||||
Timepoint forward = new Timepoint(time);
|
||||
Timepoint backward = new Timepoint(time);
|
||||
int iteration = 0;
|
||||
int resolutionMultiplier = 1;
|
||||
if (resolution == Timepoint.TYPE.MINUTE) resolutionMultiplier = 60;
|
||||
if (resolution == Timepoint.TYPE.SECOND) resolutionMultiplier = 3600;
|
||||
|
||||
while (iteration < 24 * resolutionMultiplier) {
|
||||
iteration++;
|
||||
forward.add(resolution, 1);
|
||||
backward.add(resolution, -1);
|
||||
|
||||
if (type == null || forward.get(type) == time.get(type)) {
|
||||
Timepoint forwardCeil = mDisabledTimes.ceiling(forward);
|
||||
Timepoint forwardFloor = mDisabledTimes.floor(forward);
|
||||
if (!forward.equals(forwardCeil, resolution) && !forward.equals(forwardFloor, resolution))
|
||||
return forward;
|
||||
}
|
||||
|
||||
if (type == null || backward.get(type) == time.get(type)) {
|
||||
Timepoint backwardCeil = mDisabledTimes.ceiling(backward);
|
||||
Timepoint backwardFloor = mDisabledTimes.floor(backward);
|
||||
if (!backward.equals(backwardCeil, resolution) && !backward.equals(backwardFloor, resolution))
|
||||
return backward;
|
||||
}
|
||||
|
||||
if (type != null && backward.get(type) != time.get(type) && forward.get(type) != time.get(type))
|
||||
break;
|
||||
}
|
||||
// If this step is reached, the user has disabled all timepoints
|
||||
return time;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.wdullaer.materialdatetimepicker.time;
|
||||
|
||||
import android.animation.Keyframe;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.R;
|
||||
import com.wdullaer.materialdatetimepicker.Utils;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* View to show what number is selected. This will draw a blue circle over the number, with a blue
|
||||
* line coming from the center of the main circle to the edge of the blue selection.
|
||||
*/
|
||||
public class RadialSelectorView extends View {
|
||||
private static final String TAG = "RadialSelectorView";
|
||||
|
||||
// Alpha level for selected circle.
|
||||
private static final int SELECTED_ALPHA = Utils.SELECTED_ALPHA;
|
||||
private static final int SELECTED_ALPHA_THEME_DARK = Utils.SELECTED_ALPHA_THEME_DARK;
|
||||
// Alpha level for the line.
|
||||
private static final int FULL_ALPHA = Utils.FULL_ALPHA;
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
|
||||
private boolean mIsInitialized;
|
||||
private boolean mDrawValuesReady;
|
||||
|
||||
private float mCircleRadiusMultiplier;
|
||||
private float mAmPmCircleRadiusMultiplier;
|
||||
private float mInnerNumbersRadiusMultiplier;
|
||||
private float mOuterNumbersRadiusMultiplier;
|
||||
private float mNumbersRadiusMultiplier;
|
||||
private float mSelectionRadiusMultiplier;
|
||||
private float mAnimationRadiusMultiplier;
|
||||
private boolean mIs24HourMode;
|
||||
private boolean mHasInnerCircle;
|
||||
private int mSelectionAlpha;
|
||||
|
||||
private int mXCenter;
|
||||
private int mYCenter;
|
||||
private int mCircleRadius;
|
||||
private float mTransitionMidRadiusMultiplier;
|
||||
private float mTransitionEndRadiusMultiplier;
|
||||
private int mLineLength;
|
||||
private int mSelectionRadius;
|
||||
private InvalidateUpdateListener mInvalidateUpdateListener;
|
||||
|
||||
private int mSelectionDegrees;
|
||||
private double mSelectionRadians;
|
||||
private boolean mForceDrawDot;
|
||||
|
||||
public RadialSelectorView(Context context) {
|
||||
super(context);
|
||||
mIsInitialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this selector with the state of the picker.
|
||||
* @param context Current context.
|
||||
* @param controller Structure containing the accentColor and the 24-hour mode, which will tell us
|
||||
* whether the circle's center is moved up slightly to make room for the AM/PM circles.
|
||||
* @param hasInnerCircle Whether we have both an inner and an outer circle of numbers
|
||||
* that may be selected. Should be true for 24-hour mode in the hours circle.
|
||||
* @param disappearsOut Whether the numbers' animation will have them disappearing out
|
||||
* or disappearing in.
|
||||
* @param selectionDegrees The initial degrees to be selected.
|
||||
* @param isInnerCircle Whether the initial selection is in the inner or outer circle.
|
||||
* Will be ignored when hasInnerCircle is false.
|
||||
*/
|
||||
public void initialize(Context context, TimePickerController controller, boolean hasInnerCircle,
|
||||
boolean disappearsOut, int selectionDegrees, boolean isInnerCircle) {
|
||||
if (mIsInitialized) {
|
||||
Log.e(TAG, "This RadialSelectorView may only be initialized once.");
|
||||
return;
|
||||
}
|
||||
|
||||
Resources res = context.getResources();
|
||||
|
||||
int accentColor = controller.getAccentColor();
|
||||
mPaint.setColor(accentColor);
|
||||
mPaint.setAntiAlias(true);
|
||||
|
||||
mSelectionAlpha = controller.isThemeDark() ? SELECTED_ALPHA_THEME_DARK : SELECTED_ALPHA;
|
||||
|
||||
// Calculate values for the circle radius size.
|
||||
mIs24HourMode = controller.is24HourMode();
|
||||
if (mIs24HourMode || controller.getVersion() != TimePickerDialog.Version.VERSION_1) {
|
||||
mCircleRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_circle_radius_multiplier_24HourMode));
|
||||
} else {
|
||||
mCircleRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_circle_radius_multiplier));
|
||||
mAmPmCircleRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier));
|
||||
}
|
||||
|
||||
// Calculate values for the radius size(s) of the numbers circle(s).
|
||||
mHasInnerCircle = hasInnerCircle;
|
||||
if (hasInnerCircle) {
|
||||
mInnerNumbersRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_numbers_radius_multiplier_inner));
|
||||
mOuterNumbersRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_numbers_radius_multiplier_outer));
|
||||
} else {
|
||||
mNumbersRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_numbers_radius_multiplier_normal));
|
||||
}
|
||||
mSelectionRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_selection_radius_multiplier));
|
||||
|
||||
// Calculate values for the transition mid-way states.
|
||||
mAnimationRadiusMultiplier = 1;
|
||||
mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
|
||||
mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
|
||||
mInvalidateUpdateListener = new InvalidateUpdateListener(this);
|
||||
|
||||
setSelection(selectionDegrees, isInnerCircle, false);
|
||||
mIsInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selection.
|
||||
* @param selectionDegrees The degrees to be selected.
|
||||
* @param isInnerCircle Whether the selection should be in the inner circle or outer. Will be
|
||||
* ignored if hasInnerCircle was initialized to false.
|
||||
* @param forceDrawDot Whether to force the dot in the center of the selection circle to be
|
||||
* drawn. If false, the dot will be drawn only when the degrees is not a multiple of 30, i.e.
|
||||
* the selection is not on a visible number.
|
||||
*/
|
||||
public void setSelection(int selectionDegrees, boolean isInnerCircle, boolean forceDrawDot) {
|
||||
mSelectionDegrees = selectionDegrees;
|
||||
mSelectionRadians = selectionDegrees * Math.PI / 180;
|
||||
mForceDrawDot = forceDrawDot;
|
||||
|
||||
if (mHasInnerCircle) {
|
||||
if (isInnerCircle) {
|
||||
mNumbersRadiusMultiplier = mInnerNumbersRadiusMultiplier;
|
||||
} else {
|
||||
mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for smoother animations.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasOverlappingRendering() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the multiplier for the radius. Will be used during animations to move in/out.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) {
|
||||
mAnimationRadiusMultiplier = animationRadiusMultiplier;
|
||||
}
|
||||
|
||||
public int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
|
||||
final Boolean[] isInnerCircle) {
|
||||
if (!mDrawValuesReady) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
double hypotenuse = Math.sqrt(
|
||||
(pointY - mYCenter)*(pointY - mYCenter) +
|
||||
(pointX - mXCenter)*(pointX - mXCenter));
|
||||
// Check if we're outside the range
|
||||
if (mHasInnerCircle) {
|
||||
if (forceLegal) {
|
||||
// If we're told to force the coordinates to be legal, we'll set the isInnerCircle
|
||||
// boolean based based off whichever number the coordinates are closer to.
|
||||
int innerNumberRadius = (int) (mCircleRadius * mInnerNumbersRadiusMultiplier);
|
||||
int distanceToInnerNumber = (int) Math.abs(hypotenuse - innerNumberRadius);
|
||||
int outerNumberRadius = (int) (mCircleRadius * mOuterNumbersRadiusMultiplier);
|
||||
int distanceToOuterNumber = (int) Math.abs(hypotenuse - outerNumberRadius);
|
||||
|
||||
isInnerCircle[0] = (distanceToInnerNumber <= distanceToOuterNumber);
|
||||
} else {
|
||||
// Otherwise, if we're close enough to either number (with the space between the
|
||||
// two allotted equally), set the isInnerCircle boolean as the closer one.
|
||||
// appropriately, but otherwise return -1.
|
||||
int minAllowedHypotenuseForInnerNumber =
|
||||
(int) (mCircleRadius * mInnerNumbersRadiusMultiplier) - mSelectionRadius;
|
||||
int maxAllowedHypotenuseForOuterNumber =
|
||||
(int) (mCircleRadius * mOuterNumbersRadiusMultiplier) + mSelectionRadius;
|
||||
int halfwayHypotenusePoint = (int) (mCircleRadius *
|
||||
((mOuterNumbersRadiusMultiplier + mInnerNumbersRadiusMultiplier) / 2));
|
||||
|
||||
if (hypotenuse >= minAllowedHypotenuseForInnerNumber &&
|
||||
hypotenuse <= halfwayHypotenusePoint) {
|
||||
isInnerCircle[0] = true;
|
||||
} else if (hypotenuse <= maxAllowedHypotenuseForOuterNumber &&
|
||||
hypotenuse >= halfwayHypotenusePoint) {
|
||||
isInnerCircle[0] = false;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's just one circle, we'll need to return -1 if:
|
||||
// we're not told to force the coordinates to be legal, and
|
||||
// the coordinates' distance to the number is within the allowed distance.
|
||||
if (!forceLegal) {
|
||||
int distanceToNumber = (int) Math.abs(hypotenuse - mLineLength);
|
||||
// The max allowed distance will be defined as the distance from the center of the
|
||||
// number to the edge of the circle.
|
||||
int maxAllowedDistance = (int) (mCircleRadius * (1 - mNumbersRadiusMultiplier));
|
||||
if (distanceToNumber > maxAllowedDistance) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float opposite = Math.abs(pointY - mYCenter);
|
||||
double radians = Math.asin(opposite / hypotenuse);
|
||||
int degrees = (int) (radians * 180 / Math.PI);
|
||||
|
||||
// Now we have to translate to the correct quadrant.
|
||||
boolean rightSide = (pointX > mXCenter);
|
||||
boolean topSide = (pointY < 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;
|
||||
}
|
||||
return degrees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
int viewWidth = getWidth();
|
||||
if (viewWidth == 0 || !mIsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mDrawValuesReady) {
|
||||
mXCenter = getWidth() / 2;
|
||||
mYCenter = getHeight() / 2;
|
||||
mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier);
|
||||
|
||||
if (!mIs24HourMode) {
|
||||
// We'll need to draw the AM/PM circles, so the main circle will need to have
|
||||
// a slightly higher center. To keep the entire view centered vertically, we'll
|
||||
// have to push it up by half the radius of the AM/PM circles.
|
||||
int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier);
|
||||
mYCenter -= amPmCircleRadius *0.75;
|
||||
}
|
||||
|
||||
mSelectionRadius = (int) (mCircleRadius * mSelectionRadiusMultiplier);
|
||||
|
||||
mDrawValuesReady = true;
|
||||
}
|
||||
|
||||
// Calculate the current radius at which to place the selection circle.
|
||||
mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier);
|
||||
int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians));
|
||||
int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians));
|
||||
|
||||
// Draw the selection circle.
|
||||
mPaint.setAlpha(mSelectionAlpha);
|
||||
canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint);
|
||||
|
||||
if (mForceDrawDot | mSelectionDegrees % 30 != 0) {
|
||||
// We're not on a direct tick (or we've been told to draw the dot anyway).
|
||||
mPaint.setAlpha(FULL_ALPHA);
|
||||
canvas.drawCircle(pointX, pointY, (mSelectionRadius * 2 / 7), mPaint);
|
||||
} else {
|
||||
// We're not drawing the dot, so shorten the line to only go as far as the edge of the
|
||||
// selection circle.
|
||||
int lineLength = mLineLength;
|
||||
lineLength -= mSelectionRadius;
|
||||
pointX = mXCenter + (int) (lineLength * Math.sin(mSelectionRadians));
|
||||
pointY = mYCenter - (int) (lineLength * Math.cos(mSelectionRadians));
|
||||
}
|
||||
|
||||
// Draw the line from the center of the circle.
|
||||
mPaint.setAlpha(255);
|
||||
mPaint.setStrokeWidth(3);
|
||||
canvas.drawLine(mXCenter, mYCenter, pointX, pointY, mPaint);
|
||||
}
|
||||
|
||||
public ObjectAnimator getDisappearAnimator() {
|
||||
if (!mIsInitialized || !mDrawValuesReady) {
|
||||
Log.e(TAG, "RadialSelectorView was not ready for animation.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Keyframe kf0, kf1, kf2;
|
||||
float midwayPoint = 0.2f;
|
||||
int duration = 500;
|
||||
|
||||
kf0 = Keyframe.ofFloat(0f, 1);
|
||||
kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
|
||||
kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier);
|
||||
PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
|
||||
"animationRadiusMultiplier", kf0, kf1, kf2);
|
||||
|
||||
kf0 = Keyframe.ofFloat(0f, 1f);
|
||||
kf1 = Keyframe.ofFloat(1f, 0f);
|
||||
PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1);
|
||||
|
||||
ObjectAnimator disappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
this, radiusDisappear, fadeOut).setDuration(duration);
|
||||
disappearAnimator.addUpdateListener(mInvalidateUpdateListener);
|
||||
|
||||
return disappearAnimator;
|
||||
}
|
||||
|
||||
public ObjectAnimator getReappearAnimator() {
|
||||
if (!mIsInitialized || !mDrawValuesReady) {
|
||||
Log.e(TAG, "RadialSelectorView was not ready for animation.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Keyframe kf0, kf1, kf2, kf3;
|
||||
float midwayPoint = 0.2f;
|
||||
int duration = 500;
|
||||
|
||||
// The time points are half of what they would normally be, because this animation is
|
||||
// staggered against the disappear so they happen seamlessly. The reappear starts
|
||||
// halfway into the disappear.
|
||||
float delayMultiplier = 0.25f;
|
||||
float transitionDurationMultiplier = 1f;
|
||||
float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
|
||||
int totalDuration = (int) (duration * totalDurationMultiplier);
|
||||
float delayPoint = (delayMultiplier * duration) / totalDuration;
|
||||
midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
|
||||
|
||||
kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier);
|
||||
kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier);
|
||||
kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
|
||||
kf3 = Keyframe.ofFloat(1f, 1);
|
||||
PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
|
||||
"animationRadiusMultiplier", kf0, kf1, kf2, kf3);
|
||||
|
||||
kf0 = Keyframe.ofFloat(0f, 0f);
|
||||
kf1 = Keyframe.ofFloat(delayPoint, 0f);
|
||||
kf2 = Keyframe.ofFloat(1f, 1f);
|
||||
PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
|
||||
|
||||
ObjectAnimator reappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
this, radiusReappear, fadeIn).setDuration(totalDuration);
|
||||
reappearAnimator.addUpdateListener(mInvalidateUpdateListener);
|
||||
return reappearAnimator;
|
||||
}
|
||||
|
||||
/**
|
||||
* We'll need to invalidate during the animation.
|
||||
*/
|
||||
private static class InvalidateUpdateListener implements AnimatorUpdateListener {
|
||||
private final WeakReference<RadialSelectorView> selectorRef;
|
||||
|
||||
InvalidateUpdateListener(RadialSelectorView selectorView) {
|
||||
this.selectorRef = new WeakReference<>(selectorView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
RadialSelectorView selectorView = selectorRef.get();
|
||||
if (selectorView != null) {
|
||||
selectorView.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.wdullaer.materialdatetimepicker.time;
|
||||
|
||||
import android.animation.Keyframe;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.Paint.Align;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.R;
|
||||
|
||||
/**
|
||||
* A view to show a series of numbers in a circular pattern.
|
||||
*/
|
||||
public class RadialTextsView extends View {
|
||||
private final static String TAG = "RadialTextsView";
|
||||
|
||||
private final Paint mPaint = new Paint();
|
||||
private final Paint mSelectedPaint = new Paint();
|
||||
private final Paint mInactivePaint = new Paint();
|
||||
|
||||
private boolean mDrawValuesReady;
|
||||
private boolean mIsInitialized;
|
||||
|
||||
private int selection = -1;
|
||||
|
||||
private SelectionValidator mValidator;
|
||||
|
||||
private Typeface mTypefaceLight;
|
||||
private Typeface mTypefaceRegular;
|
||||
private String[] mTexts;
|
||||
private String[] mInnerTexts;
|
||||
private boolean mIs24HourMode;
|
||||
private boolean mHasInnerCircle;
|
||||
private float mCircleRadiusMultiplier;
|
||||
private float mAmPmCircleRadiusMultiplier;
|
||||
private float mNumbersRadiusMultiplier;
|
||||
private float mInnerNumbersRadiusMultiplier;
|
||||
private float mTextSizeMultiplier;
|
||||
private float mInnerTextSizeMultiplier;
|
||||
|
||||
private int mXCenter;
|
||||
private int mYCenter;
|
||||
private float mCircleRadius;
|
||||
private boolean mTextGridValuesDirty;
|
||||
private float mTextSize;
|
||||
private float mInnerTextSize;
|
||||
private float[] mTextGridHeights;
|
||||
private float[] mTextGridWidths;
|
||||
private float[] mInnerTextGridHeights;
|
||||
private float[] mInnerTextGridWidths;
|
||||
|
||||
private float mAnimationRadiusMultiplier;
|
||||
private float mTransitionMidRadiusMultiplier;
|
||||
private float mTransitionEndRadiusMultiplier;
|
||||
ObjectAnimator mDisappearAnimator;
|
||||
ObjectAnimator mReappearAnimator;
|
||||
private InvalidateUpdateListener mInvalidateUpdateListener;
|
||||
|
||||
public RadialTextsView(Context context) {
|
||||
super(context);
|
||||
mIsInitialized = false;
|
||||
}
|
||||
|
||||
public void initialize(Context context, String[] texts, String[] innerTexts,
|
||||
TimePickerController controller, SelectionValidator validator, boolean disappearsOut) {
|
||||
if (mIsInitialized) {
|
||||
Log.e(TAG, "This RadialTextsView may only be initialized once.");
|
||||
return;
|
||||
}
|
||||
Resources res = context.getResources();
|
||||
|
||||
// Set up the paint.
|
||||
int textColorRes = controller.isThemeDark() ? R.color.mdtp_white : R.color.mdtp_numbers_text_color;
|
||||
mPaint.setColor(ContextCompat.getColor(context, textColorRes));
|
||||
String typefaceFamily = res.getString(R.string.mdtp_radial_numbers_typeface);
|
||||
mTypefaceLight = Typeface.create(typefaceFamily, Typeface.NORMAL);
|
||||
String typefaceFamilyRegular = res.getString(R.string.mdtp_sans_serif);
|
||||
mTypefaceRegular = Typeface.create(typefaceFamilyRegular, Typeface.NORMAL);
|
||||
mPaint.setAntiAlias(true);
|
||||
mPaint.setTextAlign(Align.CENTER);
|
||||
|
||||
// Set up the selected paint
|
||||
int selectedTextColor = ContextCompat.getColor(context, R.color.mdtp_white);
|
||||
mSelectedPaint.setColor(selectedTextColor);
|
||||
mSelectedPaint.setAntiAlias(true);
|
||||
mSelectedPaint.setTextAlign(Align.CENTER);
|
||||
|
||||
// Set up the inactive paint
|
||||
int inactiveColorRes = controller.isThemeDark() ? R.color.mdtp_date_picker_text_disabled_dark_theme
|
||||
: R.color.mdtp_date_picker_text_disabled;
|
||||
mInactivePaint.setColor(ContextCompat.getColor(context, inactiveColorRes));
|
||||
mInactivePaint.setAntiAlias(true);
|
||||
mInactivePaint.setTextAlign(Align.CENTER);
|
||||
|
||||
mTexts = texts;
|
||||
mInnerTexts = innerTexts;
|
||||
mIs24HourMode = controller.is24HourMode();
|
||||
mHasInnerCircle = (innerTexts != null);
|
||||
|
||||
// Calculate the radius for the main circle.
|
||||
if (mIs24HourMode || controller.getVersion() != TimePickerDialog.Version.VERSION_1) {
|
||||
mCircleRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_circle_radius_multiplier_24HourMode));
|
||||
} else {
|
||||
mCircleRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_circle_radius_multiplier));
|
||||
mAmPmCircleRadiusMultiplier =
|
||||
Float.parseFloat(res.getString(R.string.mdtp_ampm_circle_radius_multiplier));
|
||||
}
|
||||
|
||||
// Initialize the widths and heights of the grid, and calculate the values for the numbers.
|
||||
mTextGridHeights = new float[7];
|
||||
mTextGridWidths = new float[7];
|
||||
if (mHasInnerCircle) {
|
||||
mNumbersRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_numbers_radius_multiplier_outer));
|
||||
mInnerNumbersRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_numbers_radius_multiplier_inner));
|
||||
|
||||
// Version 2 layout draws outer circle bigger than inner
|
||||
if (controller.getVersion() == TimePickerDialog.Version.VERSION_1) {
|
||||
mTextSizeMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_text_size_multiplier_outer));
|
||||
mInnerTextSizeMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_text_size_multiplier_inner));
|
||||
} else {
|
||||
mTextSizeMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_text_size_multiplier_outer_v2));
|
||||
mInnerTextSizeMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_text_size_multiplier_inner_v2));
|
||||
}
|
||||
|
||||
mInnerTextGridHeights = new float[7];
|
||||
mInnerTextGridWidths = new float[7];
|
||||
} else {
|
||||
mNumbersRadiusMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_numbers_radius_multiplier_normal));
|
||||
mTextSizeMultiplier = Float.parseFloat(
|
||||
res.getString(R.string.mdtp_text_size_multiplier_normal));
|
||||
}
|
||||
|
||||
mAnimationRadiusMultiplier = 1;
|
||||
mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut? -1 : 1));
|
||||
mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut? 1 : -1));
|
||||
mInvalidateUpdateListener = new InvalidateUpdateListener();
|
||||
|
||||
mValidator = validator;
|
||||
|
||||
mTextGridValuesDirty = true;
|
||||
mIsInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the selected text. Depending on the theme this will be rendered differently
|
||||
* @param selection The text which is currently selected
|
||||
*/
|
||||
protected void setSelection(int selection) {
|
||||
this.selection = selection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows for smoother animation.
|
||||
*/
|
||||
@Override
|
||||
public boolean hasOverlappingRendering() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the animation to move the numbers in and out.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public void setAnimationRadiusMultiplier(float animationRadiusMultiplier) {
|
||||
mAnimationRadiusMultiplier = animationRadiusMultiplier;
|
||||
mTextGridValuesDirty = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
int viewWidth = getWidth();
|
||||
if (viewWidth == 0 || !mIsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mDrawValuesReady) {
|
||||
mXCenter = getWidth() / 2;
|
||||
mYCenter = getHeight() / 2;
|
||||
mCircleRadius = Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier;
|
||||
if (!mIs24HourMode) {
|
||||
// We'll need to draw the AM/PM circles, so the main circle will need to have
|
||||
// a slightly higher center. To keep the entire view centered vertically, we'll
|
||||
// have to push it up by half the radius of the AM/PM circles.
|
||||
float amPmCircleRadius = mCircleRadius * mAmPmCircleRadiusMultiplier;
|
||||
mYCenter -= amPmCircleRadius *0.75;
|
||||
}
|
||||
|
||||
mTextSize = mCircleRadius * mTextSizeMultiplier;
|
||||
if (mHasInnerCircle) {
|
||||
mInnerTextSize = mCircleRadius * mInnerTextSizeMultiplier;
|
||||
}
|
||||
|
||||
// Because the text positions will be static, pre-render the animations.
|
||||
renderAnimations();
|
||||
|
||||
mTextGridValuesDirty = true;
|
||||
mDrawValuesReady = true;
|
||||
}
|
||||
|
||||
// Calculate the text positions, but only if they've changed since the last onDraw.
|
||||
if (mTextGridValuesDirty) {
|
||||
float numbersRadius =
|
||||
mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
|
||||
|
||||
// Calculate the positions for the 12 numbers in the main circle.
|
||||
calculateGridSizes(numbersRadius, mXCenter, mYCenter,
|
||||
mTextSize, mTextGridHeights, mTextGridWidths);
|
||||
if (mHasInnerCircle) {
|
||||
// If we have an inner circle, calculate those positions too.
|
||||
float innerNumbersRadius =
|
||||
mCircleRadius * mInnerNumbersRadiusMultiplier * mAnimationRadiusMultiplier;
|
||||
calculateGridSizes(innerNumbersRadius, mXCenter, mYCenter,
|
||||
mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths);
|
||||
}
|
||||
mTextGridValuesDirty = false;
|
||||
}
|
||||
|
||||
// Draw the texts in the pre-calculated positions.
|
||||
drawTexts(canvas, mTextSize, mTypefaceLight, mTexts, mTextGridWidths, mTextGridHeights);
|
||||
if (mHasInnerCircle) {
|
||||
drawTexts(canvas, mInnerTextSize, mTypefaceRegular, mInnerTexts,
|
||||
mInnerTextGridWidths, mInnerTextGridHeights);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the trigonometric Unit Circle, calculate the positions that the text will need to be
|
||||
* drawn at based on the specified circle radius. Place the values in the textGridHeights and
|
||||
* textGridWidths parameters.
|
||||
*/
|
||||
private void calculateGridSizes(float numbersRadius, float xCenter, float yCenter,
|
||||
float textSize, float[] textGridHeights, float[] textGridWidths) {
|
||||
/*
|
||||
* The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle.
|
||||
*/
|
||||
float offset1 = numbersRadius;
|
||||
// cos(30) = a / r => r * cos(30) = a => r * √3/2 = a
|
||||
float offset2 = numbersRadius * ((float) Math.sqrt(3)) / 2f;
|
||||
// sin(30) = o / r => r * sin(30) = o => r / 2 = a
|
||||
float offset3 = numbersRadius / 2f;
|
||||
mPaint.setTextSize(textSize);
|
||||
mSelectedPaint.setTextSize(textSize);
|
||||
mInactivePaint.setTextSize(textSize);
|
||||
// We'll need yTextBase to be slightly lower to account for the text's baseline.
|
||||
yCenter -= (mPaint.descent() + mPaint.ascent()) / 2;
|
||||
|
||||
textGridHeights[0] = yCenter - offset1;
|
||||
textGridWidths[0] = xCenter - offset1;
|
||||
textGridHeights[1] = yCenter - offset2;
|
||||
textGridWidths[1] = xCenter - offset2;
|
||||
textGridHeights[2] = yCenter - offset3;
|
||||
textGridWidths[2] = xCenter - offset3;
|
||||
textGridHeights[3] = yCenter;
|
||||
textGridWidths[3] = xCenter;
|
||||
textGridHeights[4] = yCenter + offset3;
|
||||
textGridWidths[4] = xCenter + offset3;
|
||||
textGridHeights[5] = yCenter + offset2;
|
||||
textGridWidths[5] = xCenter + offset2;
|
||||
textGridHeights[6] = yCenter + offset1;
|
||||
textGridWidths[6] = xCenter + offset1;
|
||||
}
|
||||
|
||||
private Paint[] assignTextColors(String[] texts) {
|
||||
Paint[] paints = new Paint[texts.length];
|
||||
for(int i=0;i<texts.length;i++) {
|
||||
int text = Integer.parseInt(texts[i]);
|
||||
if(text == selection) paints[i] = mSelectedPaint;
|
||||
else if(mValidator.isValidSelection(text)) paints[i] = mPaint;
|
||||
else paints[i] = mInactivePaint;
|
||||
}
|
||||
return paints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the 12 text values at the positions specified by the textGrid parameters.
|
||||
*/
|
||||
private void drawTexts(Canvas canvas, float textSize, Typeface typeface, String[] texts,
|
||||
float[] textGridWidths, float[] textGridHeights) {
|
||||
mPaint.setTextSize(textSize);
|
||||
mPaint.setTypeface(typeface);
|
||||
Paint[] textPaints = assignTextColors(texts);
|
||||
canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], textPaints[0]);
|
||||
canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], textPaints[1]);
|
||||
canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], textPaints[2]);
|
||||
canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], textPaints[3]);
|
||||
canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], textPaints[4]);
|
||||
canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], textPaints[5]);
|
||||
canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], textPaints[6]);
|
||||
canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], textPaints[7]);
|
||||
canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], textPaints[8]);
|
||||
canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], textPaints[9]);
|
||||
canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], textPaints[10]);
|
||||
canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], textPaints[11]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the animations for appearing and disappearing.
|
||||
*/
|
||||
private void renderAnimations() {
|
||||
Keyframe kf0, kf1, kf2, kf3;
|
||||
float midwayPoint = 0.2f;
|
||||
int duration = 500;
|
||||
|
||||
// Set up animator for disappearing.
|
||||
kf0 = Keyframe.ofFloat(0f, 1);
|
||||
kf1 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
|
||||
kf2 = Keyframe.ofFloat(1f, mTransitionEndRadiusMultiplier);
|
||||
PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
|
||||
"animationRadiusMultiplier", kf0, kf1, kf2);
|
||||
|
||||
kf0 = Keyframe.ofFloat(0f, 1f);
|
||||
kf1 = Keyframe.ofFloat(1f, 0f);
|
||||
PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1);
|
||||
|
||||
mDisappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
this, radiusDisappear, fadeOut).setDuration(duration);
|
||||
mDisappearAnimator.addUpdateListener(mInvalidateUpdateListener);
|
||||
|
||||
|
||||
// Set up animator for reappearing.
|
||||
float delayMultiplier = 0.25f;
|
||||
float transitionDurationMultiplier = 1f;
|
||||
float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
|
||||
int totalDuration = (int) (duration * totalDurationMultiplier);
|
||||
float delayPoint = (delayMultiplier * duration) / totalDuration;
|
||||
midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
|
||||
|
||||
kf0 = Keyframe.ofFloat(0f, mTransitionEndRadiusMultiplier);
|
||||
kf1 = Keyframe.ofFloat(delayPoint, mTransitionEndRadiusMultiplier);
|
||||
kf2 = Keyframe.ofFloat(midwayPoint, mTransitionMidRadiusMultiplier);
|
||||
kf3 = Keyframe.ofFloat(1f, 1);
|
||||
PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
|
||||
"animationRadiusMultiplier", kf0, kf1, kf2, kf3);
|
||||
|
||||
kf0 = Keyframe.ofFloat(0f, 0f);
|
||||
kf1 = Keyframe.ofFloat(delayPoint, 0f);
|
||||
kf2 = Keyframe.ofFloat(1f, 1f);
|
||||
PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
|
||||
|
||||
mReappearAnimator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
this, radiusReappear, fadeIn).setDuration(totalDuration);
|
||||
mReappearAnimator.addUpdateListener(mInvalidateUpdateListener);
|
||||
}
|
||||
|
||||
public ObjectAnimator getDisappearAnimator() {
|
||||
if (!mIsInitialized || !mDrawValuesReady || mDisappearAnimator == null) {
|
||||
Log.e(TAG, "RadialTextView was not ready for animation.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return mDisappearAnimator;
|
||||
}
|
||||
|
||||
public ObjectAnimator getReappearAnimator() {
|
||||
if (!mIsInitialized || !mDrawValuesReady || mReappearAnimator == null) {
|
||||
Log.e(TAG, "RadialTextView was not ready for animation.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return mReappearAnimator;
|
||||
}
|
||||
|
||||
private class InvalidateUpdateListener implements AnimatorUpdateListener {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
RadialTextsView.this.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectionValidator {
|
||||
boolean isValidSelection(int selection);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.wdullaer.materialdatetimepicker.time;
|
||||
|
||||
/**
|
||||
* A collection of methods which need to be shared with all components of the TimePicker
|
||||
*
|
||||
* Created by wdullaer on 6/10/15.
|
||||
*/
|
||||
interface TimePickerController {
|
||||
/**
|
||||
* @return boolean - true if the dark theme should be used
|
||||
*/
|
||||
boolean isThemeDark();
|
||||
|
||||
/**
|
||||
* @return boolean - true if 24 hour mode is used / false if AM/PM is used
|
||||
*/
|
||||
boolean is24HourMode();
|
||||
|
||||
/**
|
||||
* @return int - the accent color currently in use
|
||||
*/
|
||||
int getAccentColor();
|
||||
|
||||
/**
|
||||
* @return Version - The current version to render
|
||||
*/
|
||||
TimePickerDialog.Version getVersion();
|
||||
|
||||
/**
|
||||
* Request the device to vibrate
|
||||
*/
|
||||
void tryVibrate();
|
||||
|
||||
/**
|
||||
* @param time Timepoint - the selected point in time
|
||||
* @param index int - The current view to consider when calculating the range
|
||||
* @return boolean - true if this is not a selectable value
|
||||
*/
|
||||
boolean isOutOfRange(Timepoint time, int index);
|
||||
|
||||
/**
|
||||
* @return boolean - true if AM times are outside the range of valid selections
|
||||
*/
|
||||
boolean isAmDisabled();
|
||||
|
||||
/**
|
||||
* @return boolean - true if PM times are outside the range of valid selections
|
||||
*/
|
||||
boolean isPmDisabled();
|
||||
|
||||
/**
|
||||
* Will round the given Timepoint to the nearest valid Timepoint given the following restrictions:
|
||||
* - TYPE.HOUR, it will just round to the next valid point, possible adjusting minutes and seconds
|
||||
* - TYPE.MINUTE, it will round to the next valid point, without adjusting the hour, but possibly adjusting the seconds
|
||||
* - TYPE.SECOND, it will round to the next valid point, only adjusting the seconds
|
||||
* @param time Timepoint - the timepoint to validate
|
||||
* @param type Timepoint.TYPE - whether we should round the hours, minutes or seconds
|
||||
* @return timepoint - the nearest valid timepoint
|
||||
*/
|
||||
Timepoint roundToNearest(Timepoint time, Timepoint.TYPE type);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
package com.wdullaer.materialdatetimepicker.time;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static com.wdullaer.materialdatetimepicker.time.Timepoint.TYPE.HOUR;
|
||||
import static com.wdullaer.materialdatetimepicker.time.Timepoint.TYPE.MINUTE;
|
||||
|
||||
/**
|
||||
* Simple utility class that represents a time in the day up to second precision
|
||||
* The time input is expected to use 24 hour mode.
|
||||
* Fields are modulo'd into their correct ranges.
|
||||
* It does not handle timezones.
|
||||
*
|
||||
* Created by wdullaer on 13/10/15.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class Timepoint implements Parcelable, Comparable<Timepoint> {
|
||||
private int hour;
|
||||
private int minute;
|
||||
private int second;
|
||||
|
||||
public enum TYPE {
|
||||
HOUR,
|
||||
MINUTE,
|
||||
SECOND
|
||||
}
|
||||
|
||||
public Timepoint(Timepoint time) {
|
||||
this(time.hour, time.minute, time.second);
|
||||
}
|
||||
|
||||
public Timepoint(@IntRange(from=0, to=23) int hour,
|
||||
@IntRange(from=0, to=59) int minute,
|
||||
@IntRange(from=0, to=59) int second) {
|
||||
this.hour = hour % 24;
|
||||
this.minute = minute % 60;
|
||||
this.second = second % 60;
|
||||
}
|
||||
|
||||
public Timepoint(@IntRange(from=0, to=23) int hour,
|
||||
@IntRange(from=0, to=59) int minute) {
|
||||
this(hour, minute, 0);
|
||||
}
|
||||
|
||||
public Timepoint(@IntRange(from=0, to=23) int hour) {
|
||||
this(hour, 0);
|
||||
}
|
||||
|
||||
public Timepoint(Parcel in) {
|
||||
hour = in.readInt();
|
||||
minute = in.readInt();
|
||||
second = in.readInt();
|
||||
}
|
||||
|
||||
@IntRange(from=0, to=23)
|
||||
public int getHour() {
|
||||
return hour;
|
||||
}
|
||||
|
||||
@IntRange(from=0, to=59)
|
||||
public int getMinute() {
|
||||
return minute;
|
||||
}
|
||||
|
||||
@IntRange(from=0, to=59)
|
||||
public int getSecond() {
|
||||
return second;
|
||||
}
|
||||
|
||||
public boolean isAM() {
|
||||
return hour < 12;
|
||||
}
|
||||
|
||||
public boolean isPM() {
|
||||
return !isAM();
|
||||
}
|
||||
|
||||
public void setAM() {
|
||||
if(hour >= 12) hour = hour % 12;
|
||||
}
|
||||
|
||||
public void setPM() {
|
||||
if(hour < 12) hour = (hour + 12) % 24;
|
||||
}
|
||||
|
||||
public void add(TYPE type, int value) {
|
||||
if (type == MINUTE) value *= 60;
|
||||
if (type == HOUR) value *= 3600;
|
||||
value += toSeconds();
|
||||
|
||||
switch (type) {
|
||||
case SECOND:
|
||||
int secondVal = (value % 3600) % 60;
|
||||
if(secondVal<0){
|
||||
second = 60+secondVal;
|
||||
add(MINUTE, -1);
|
||||
}else {
|
||||
second = secondVal;
|
||||
}
|
||||
case MINUTE:
|
||||
int minuteVal = (value % 3600) / 60;
|
||||
if(minuteVal<0){
|
||||
minute = 60+minuteVal;
|
||||
add(HOUR, -1);
|
||||
}else {
|
||||
minute = minuteVal;
|
||||
}
|
||||
case HOUR:
|
||||
int hourVal = (value / 3600) % 24;
|
||||
if(hourVal<0){
|
||||
hour = 24+hourVal;
|
||||
}else{
|
||||
hour=hourVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int get(@NonNull TYPE type) {
|
||||
switch (type) {
|
||||
case SECOND:
|
||||
return getSecond();
|
||||
case MINUTE:
|
||||
return getMinute();
|
||||
case HOUR:
|
||||
default: // Makes the compiler happy
|
||||
return getHour();
|
||||
}
|
||||
}
|
||||
|
||||
public int toSeconds() {
|
||||
return 3600 * hour + 60 * minute + second;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return toSeconds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Timepoint timepoint = (Timepoint) o;
|
||||
|
||||
return hashCode() == timepoint.hashCode();
|
||||
}
|
||||
|
||||
public boolean equals(@Nullable Timepoint time, @NonNull TYPE resolution) {
|
||||
if (time == null) return false;
|
||||
boolean output = true;
|
||||
switch (resolution) {
|
||||
case SECOND:
|
||||
output = output && time.getSecond() == getSecond();
|
||||
case MINUTE:
|
||||
output = output && time.getMinute() == getMinute();
|
||||
case HOUR:
|
||||
output = output && time.getHour() == getHour();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull Timepoint t) {
|
||||
return hashCode() - t.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(hour);
|
||||
out.writeInt(minute);
|
||||
out.writeInt(second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Timepoint> CREATOR
|
||||
= new Parcelable.Creator<Timepoint>() {
|
||||
public Timepoint createFromParcel(Parcel in) {
|
||||
return new Timepoint(in);
|
||||
}
|
||||
|
||||
public Timepoint[] newArray(int size) {
|
||||
return new Timepoint[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "" + hour + "h " + minute + "m " + second + "s";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.wdullaer.materialdatetimepicker.time;
|
||||
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public interface TimepointLimiter extends Parcelable {
|
||||
/**
|
||||
* isOutOfRange indicates whether a particular timepoint is selectable or not
|
||||
* It is called multiple times in the rendering path, so it should be fast
|
||||
*
|
||||
* The index parameter indicates which picker is currently visible. This is necessary because
|
||||
* you typically only want to compare with a resolution up to the visible component. (The
|
||||
* implementation should ensure that 8 is selectable, if any valid timepoint with 8 as the hour
|
||||
* is selectable, when the hour picker is showing)
|
||||
*
|
||||
* Similarly the overall resolution of the picker is passed in, because it can impact the
|
||||
* comparisons an implementation does (especially when comparing with a disabled list)
|
||||
*
|
||||
* The scope of this method is most likely too broad, which makes it hard to reason about. It is
|
||||
* one of the main reasons the DefaultTimeLimiter implementation of this contains extensive
|
||||
* example and generate tests. The default implementation should cover 90% of use cases, but if
|
||||
* I ever notice that a substantial amount of people are trying to implement this themselves, it
|
||||
* might need to be redesigned.
|
||||
*
|
||||
* @param point A timepoint to validate
|
||||
* @param index The currently showing picker (hour, minute, second)
|
||||
* @param resolution The overall resolution of the picker
|
||||
* @return whether the Timepoint is out of range or selectable
|
||||
*/
|
||||
boolean isOutOfRange(@Nullable Timepoint point, int index, @NonNull Timepoint.TYPE resolution);
|
||||
|
||||
/**
|
||||
* isAmDisabled ndicates whether any times before midday are selectable
|
||||
* This method is called when the picker is initialized or when the user clicks / taps the AM or
|
||||
* PM buttons.
|
||||
* This means that it's result can't be updated when the picker is already being rendered
|
||||
* @return true if the AM selector should be disabled
|
||||
*/
|
||||
boolean isAmDisabled();
|
||||
|
||||
/**
|
||||
* isPmDisabled ndicates whether any times after midday are selectable
|
||||
* This method is called when the picker is initialized or when the user clicks / taps the AM or
|
||||
* PM buttons.
|
||||
* This means that it's result can't be updated when the picker is already being rendered
|
||||
* @return true if the PM selector should be disabled
|
||||
*/
|
||||
boolean isPmDisabled();
|
||||
|
||||
/**
|
||||
* roundToNearest returns the nearest selectable timepoint given a particular input
|
||||
* It is called whenever the user touches the screen, which means it can get called very
|
||||
* frequently if the user performs a drag operation
|
||||
*
|
||||
* Both the currently showing picker and the overall resolution are passed in, for similar
|
||||
* reasons as in isOutOfRange
|
||||
*
|
||||
* @param time the proposed selection
|
||||
* @param type the currently showing picker (hour, minute, second)
|
||||
* @param resolution the overall resolution of the picker
|
||||
* @return a selectable timepoint
|
||||
*/
|
||||
@NonNull Timepoint roundToNearest(
|
||||
@NonNull Timepoint time,
|
||||
@Nullable Timepoint.TYPE type,
|
||||
@NonNull Timepoint.TYPE resolution
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user