6.6.3 - Alpha2 - 本地化 Material Date Time Picker (版本 4.2.3)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
/**
|
||||
* Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility.
|
||||
*/
|
||||
public class AccessibleLinearLayout extends LinearLayout {
|
||||
|
||||
public AccessibleLinearLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
event.setClassName(Button.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setClassName(Button.class.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Button;
|
||||
|
||||
/**
|
||||
* Fake Button class, used so TextViews can announce themselves as Buttons, for accessibility.
|
||||
*/
|
||||
public class AccessibleTextView extends androidx.appcompat.widget.AppCompatTextView {
|
||||
|
||||
public AccessibleTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
event.setClassName(Button.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setClassName(Button.class.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
* Copyright (C) 2017 Wouter Dullaert
|
||||
*
|
||||
* 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 languag`e governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.wdullaer.materialdatetimepicker;
|
||||
|
||||
import android.os.Build;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearSnapHelper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.OrientationHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Enables snapping better snapping in a RecyclerView
|
||||
* Based on the code of Ruben Sousa
|
||||
* Created by wdullaer on 3/04/17.
|
||||
*/
|
||||
public class GravitySnapHelper extends LinearSnapHelper {
|
||||
|
||||
private OrientationHelper verticalHelper;
|
||||
private OrientationHelper horizontalHelper;
|
||||
private int gravity;
|
||||
private boolean isRtlHorizontal;
|
||||
private GravitySnapHelper.SnapListener listener;
|
||||
private boolean snapping;
|
||||
private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||
super.onScrollStateChanged(recyclerView, newState);
|
||||
if (newState == RecyclerView.SCROLL_STATE_SETTLING) {
|
||||
snapping = false;
|
||||
}
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE && listener != null) {
|
||||
int position = getSnappedPosition(recyclerView);
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onSnap(position);
|
||||
}
|
||||
snapping = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public GravitySnapHelper(int gravity) {
|
||||
this(gravity, null);
|
||||
}
|
||||
|
||||
public GravitySnapHelper(int gravity, SnapListener snapListener) {
|
||||
if (gravity != Gravity.START && gravity != Gravity.END
|
||||
&& gravity != Gravity.BOTTOM && gravity != Gravity.TOP) {
|
||||
throw new IllegalArgumentException("Invalid gravity value. Use START " +
|
||||
"| END | BOTTOM | TOP constants");
|
||||
}
|
||||
this.gravity = gravity;
|
||||
this.listener = snapListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
|
||||
throws IllegalStateException {
|
||||
if (recyclerView != null) {
|
||||
if ((gravity == Gravity.START || gravity == Gravity.END)
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
isRtlHorizontal
|
||||
= recyclerView.getContext().getResources().getConfiguration()
|
||||
.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
|
||||
}
|
||||
if (listener != null) {
|
||||
recyclerView.addOnScrollListener(mScrollListener);
|
||||
}
|
||||
}
|
||||
super.attachToRecyclerView(recyclerView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
|
||||
@NonNull View targetView) {
|
||||
int[] out = new int[2];
|
||||
|
||||
if (layoutManager.canScrollHorizontally()) {
|
||||
if (gravity == Gravity.START) {
|
||||
out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager), false);
|
||||
} else { // END
|
||||
out[0] = distanceToEnd(targetView, getHorizontalHelper(layoutManager), false);
|
||||
}
|
||||
} else {
|
||||
out[0] = 0;
|
||||
}
|
||||
|
||||
if (layoutManager.canScrollVertically()) {
|
||||
if (gravity == Gravity.TOP) {
|
||||
out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager), false);
|
||||
} else { // BOTTOM
|
||||
out[1] = distanceToEnd(targetView, getVerticalHelper(layoutManager), false);
|
||||
}
|
||||
} else {
|
||||
out[1] = 0;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
|
||||
View snapView = null;
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
switch (gravity) {
|
||||
case Gravity.START:
|
||||
snapView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
|
||||
break;
|
||||
case Gravity.END:
|
||||
snapView = findEndView(layoutManager, getHorizontalHelper(layoutManager));
|
||||
break;
|
||||
case Gravity.TOP:
|
||||
snapView = findStartView(layoutManager, getVerticalHelper(layoutManager));
|
||||
break;
|
||||
case Gravity.BOTTOM:
|
||||
snapView = findEndView(layoutManager, getVerticalHelper(layoutManager));
|
||||
break;
|
||||
}
|
||||
}
|
||||
snapping = snapView != null;
|
||||
return snapView;
|
||||
}
|
||||
|
||||
private int distanceToStart(View targetView, OrientationHelper helper, boolean fromEnd) {
|
||||
if (isRtlHorizontal && !fromEnd) {
|
||||
return distanceToEnd(targetView, helper, true);
|
||||
}
|
||||
|
||||
return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
|
||||
}
|
||||
|
||||
private int distanceToEnd(View targetView, OrientationHelper helper, boolean fromStart) {
|
||||
if (isRtlHorizontal && !fromStart) {
|
||||
return distanceToStart(targetView, helper, true);
|
||||
}
|
||||
|
||||
return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first view that we should snap to.
|
||||
*
|
||||
* @param layoutManager the recyclerview's layout manager
|
||||
* @param helper orientation helper to calculate view sizes
|
||||
* @return the first view in the LayoutManager to snap to
|
||||
*/
|
||||
private View findStartView(RecyclerView.LayoutManager layoutManager,
|
||||
OrientationHelper helper) {
|
||||
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
||||
|
||||
if (firstChild == RecyclerView.NO_POSITION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
View child = layoutManager.findViewByPosition(firstChild);
|
||||
|
||||
float visibleWidth;
|
||||
|
||||
// We should return the child if it's visible width
|
||||
// is greater than 0.5 of it's total width.
|
||||
// In a RTL configuration, we need to check the start point and in LTR the end point
|
||||
if (isRtlHorizontal) {
|
||||
visibleWidth = (float) (helper.getTotalSpace() - helper.getDecoratedStart(child))
|
||||
/ helper.getDecoratedMeasurement(child);
|
||||
} else {
|
||||
visibleWidth = (float) helper.getDecoratedEnd(child)
|
||||
/ helper.getDecoratedMeasurement(child);
|
||||
}
|
||||
|
||||
// If we're at the end of the list, we shouldn't snap
|
||||
// to avoid having the last item not completely visible.
|
||||
boolean endOfList = ((LinearLayoutManager) layoutManager)
|
||||
.findLastCompletelyVisibleItemPosition()
|
||||
== layoutManager.getItemCount() - 1;
|
||||
|
||||
if (visibleWidth > 0.5f && !endOfList) {
|
||||
return child;
|
||||
} else if (endOfList) {
|
||||
return null;
|
||||
} else {
|
||||
// If the child wasn't returned, we need to return
|
||||
// the next view close to the start.
|
||||
return layoutManager.findViewByPosition(firstChild + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private View findEndView(RecyclerView.LayoutManager layoutManager,
|
||||
OrientationHelper helper) {
|
||||
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
int lastChild = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
|
||||
|
||||
if (lastChild == RecyclerView.NO_POSITION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
View child = layoutManager.findViewByPosition(lastChild);
|
||||
|
||||
float visibleWidth;
|
||||
|
||||
if (isRtlHorizontal) {
|
||||
visibleWidth = (float) helper.getDecoratedEnd(child)
|
||||
/ helper.getDecoratedMeasurement(child);
|
||||
} else {
|
||||
visibleWidth = (float) (helper.getTotalSpace() - helper.getDecoratedStart(child))
|
||||
/ helper.getDecoratedMeasurement(child);
|
||||
}
|
||||
|
||||
// If we're at the start of the list, we shouldn't snap
|
||||
// to avoid having the first item not completely visible.
|
||||
boolean startOfList = ((LinearLayoutManager) layoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition() == 0;
|
||||
|
||||
if (visibleWidth > 0.5f && !startOfList) {
|
||||
return child;
|
||||
} else if (startOfList) {
|
||||
return null;
|
||||
} else {
|
||||
// If the child wasn't returned, we need to return the previous view
|
||||
return layoutManager.findViewByPosition(lastChild - 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getSnappedPosition(RecyclerView recyclerView) {
|
||||
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
if (gravity == Gravity.START || gravity == Gravity.TOP) {
|
||||
return ((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition();
|
||||
} else if (gravity == Gravity.END || gravity == Gravity.BOTTOM) {
|
||||
return ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
|
||||
}
|
||||
}
|
||||
|
||||
return RecyclerView.NO_POSITION;
|
||||
}
|
||||
|
||||
private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
|
||||
if (verticalHelper == null) {
|
||||
verticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
|
||||
}
|
||||
return verticalHelper;
|
||||
}
|
||||
|
||||
private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
|
||||
if (horizontalHelper == null) {
|
||||
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
|
||||
}
|
||||
return horizontalHelper;
|
||||
}
|
||||
|
||||
public interface SnapListener {
|
||||
void onSnap(int position);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.wdullaer.materialdatetimepicker;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Vibrator;
|
||||
import android.provider.Settings;
|
||||
|
||||
/**
|
||||
* A simple utility class to handle haptic feedback.
|
||||
*/
|
||||
public class HapticFeedbackController {
|
||||
private static final int VIBRATE_DELAY_MS = 125;
|
||||
private static final int VIBRATE_LENGTH_MS = 50;
|
||||
|
||||
private static boolean checkGlobalSetting(Context context) {
|
||||
return Settings.System.getInt(context.getContentResolver(),
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 1;
|
||||
}
|
||||
|
||||
private final Context mContext;
|
||||
private final ContentObserver mContentObserver;
|
||||
|
||||
private Vibrator mVibrator;
|
||||
private boolean mIsGloballyEnabled;
|
||||
private long mLastVibrate;
|
||||
|
||||
public HapticFeedbackController(Context context) {
|
||||
mContext = context;
|
||||
mContentObserver = new ContentObserver(null) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
mIsGloballyEnabled = checkGlobalSetting(mContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to setup the controller.
|
||||
*/
|
||||
public void start() {
|
||||
if (hasVibratePermission(mContext)) {
|
||||
mVibrator = (Vibrator) mContext.getSystemService(Service.VIBRATOR_SERVICE);
|
||||
}
|
||||
|
||||
// Setup a listener for changes in haptic feedback settings
|
||||
mIsGloballyEnabled = checkGlobalSetting(mContext);
|
||||
Uri uri = Settings.System.getUriFor(Settings.System.HAPTIC_FEEDBACK_ENABLED);
|
||||
mContext.getContentResolver().registerContentObserver(uri, false, mContentObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to verify that vibrate permission has been granted.
|
||||
*
|
||||
* Allows users of the library to disabled vibrate support if desired.
|
||||
* @return true if Vibrate permission has been granted
|
||||
*/
|
||||
private boolean hasVibratePermission(Context context) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
int hasPerm = pm.checkPermission(android.Manifest.permission.VIBRATE, context.getPackageName());
|
||||
return hasPerm == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when you don't need the controller anymore.
|
||||
*/
|
||||
public void stop() {
|
||||
mVibrator = null;
|
||||
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to vibrate. To prevent this becoming a single continuous vibration, nothing will
|
||||
* happen if we have vibrated very recently.
|
||||
*/
|
||||
public void tryVibrate() {
|
||||
if (mVibrator != null && mIsGloballyEnabled) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
// We want to try to vibrate each individual tick discretely.
|
||||
if (now - mLastVibrate >= VIBRATE_DELAY_MS) {
|
||||
mVibrator.vibrate(VIBRATE_LENGTH_MS);
|
||||
mLastVibrate = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.animation.Keyframe;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* Utility helper functions for time and date pickers.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class Utils {
|
||||
|
||||
//public static final int MONDAY_BEFORE_JULIAN_EPOCH = Time.EPOCH_JULIAN_DAY - 3;
|
||||
public static final int PULSE_ANIMATOR_DURATION = 544;
|
||||
|
||||
// Alpha level for time picker selection.
|
||||
public static final int SELECTED_ALPHA = 255;
|
||||
public static final int SELECTED_ALPHA_THEME_DARK = 255;
|
||||
// Alpha level for fully opaque.
|
||||
public static final int FULL_ALPHA = 255;
|
||||
|
||||
/**
|
||||
* Try to speak the specified text, for accessibility. Only available on JB or later.
|
||||
* @param text Text to announce.
|
||||
*/
|
||||
public static void tryAccessibilityAnnounce(View view, CharSequence text) {
|
||||
if (view != null && text != null) {
|
||||
view.announceForAccessibility(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an animator to pulsate a view in place.
|
||||
* @param labelToAnimate the view to pulsate.
|
||||
* @return The animator object. Use .start() to begin.
|
||||
*/
|
||||
public static ObjectAnimator getPulseAnimator(View labelToAnimate, float decreaseRatio,
|
||||
float increaseRatio) {
|
||||
Keyframe k0 = Keyframe.ofFloat(0f, 1f);
|
||||
Keyframe k1 = Keyframe.ofFloat(0.275f, decreaseRatio);
|
||||
Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio);
|
||||
Keyframe k3 = Keyframe.ofFloat(1f, 1f);
|
||||
|
||||
PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe("scaleX", k0, k1, k2, k3);
|
||||
PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe("scaleY", k0, k1, k2, k3);
|
||||
ObjectAnimator pulseAnimator =
|
||||
ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY);
|
||||
pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION);
|
||||
|
||||
return pulseAnimator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Dp to Pixel
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static int dpToPx(float dp, Resources resources){
|
||||
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.getDisplayMetrics());
|
||||
return (int) px;
|
||||
}
|
||||
|
||||
public static int darkenColor(int color) {
|
||||
float[] hsv = new float[3];
|
||||
Color.colorToHSV(color, hsv);
|
||||
hsv[2] = hsv[2] * 0.8f; // value component
|
||||
return Color.HSVToColor(hsv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the colorAccent from the current context, if possible/available
|
||||
* @param context The context to use as reference for the color
|
||||
* @return the accent color of the current context
|
||||
*/
|
||||
public static int getAccentColorFromThemeIfAvailable(Context context) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
// First, try the android:colorAccent
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
|
||||
return typedValue.data;
|
||||
}
|
||||
// Next, try colorAccent from support lib
|
||||
int colorAccentResId = context.getResources().getIdentifier("colorAccent", "attr", context.getPackageName());
|
||||
if (colorAccentResId != 0 && context.getTheme().resolveAttribute(colorAccentResId, typedValue, true)) {
|
||||
return typedValue.data;
|
||||
}
|
||||
// Return the value in mdtp_accent_color
|
||||
return ContextCompat.getColor(context, R.color.mdtp_accent_color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets dialog type (Light/Dark) from current theme
|
||||
* @param context The context to use as reference for the boolean
|
||||
* @param current Default value to return if cannot resolve the attribute
|
||||
* @return true if dark mode, false if light.
|
||||
*/
|
||||
public static boolean isDarkTheme(Context context, boolean current) {
|
||||
return resolveBoolean(context, R.attr.mdtp_theme_dark, current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the required boolean value from the current context, if possible/available
|
||||
* @param context The context to use as reference for the boolean
|
||||
* @param attr Attribute id to resolve
|
||||
* @param fallback Default value to return if no value is specified in theme
|
||||
* @return the boolean value from current theme
|
||||
*/
|
||||
private static boolean resolveBoolean(Context context, @AttrRes int attr, boolean fallback) {
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||
try {
|
||||
return a.getBoolean(0, fallback);
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims off all time information, effectively setting it to midnight
|
||||
* Makes it easier to compare at just the day level
|
||||
*
|
||||
* @param calendar The Calendar object to trim
|
||||
* @return The trimmed Calendar object
|
||||
*/
|
||||
public static Calendar trimToMidnight(Calendar calendar) {
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||
calendar.set(Calendar.MINUTE, 0);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
return calendar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.wdullaer.materialdatetimepicker;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.text.TextPaint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
|
||||
/**
|
||||
* TextView that renders it's contents vertically. (Just using rotate doesn't work because onMeasure
|
||||
* happens before the View is rotated causing incorrect View boundaries)
|
||||
* Created by wdullaer on 28/03/16.
|
||||
*/
|
||||
public class VerticalTextView extends androidx.appcompat.widget.AppCompatTextView {
|
||||
final boolean topDown;
|
||||
|
||||
public VerticalTextView(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
final int gravity = getGravity();
|
||||
if (Gravity.isVertical(gravity) && (gravity&Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
|
||||
setGravity((gravity&Gravity.HORIZONTAL_GRAVITY_MASK) | Gravity.TOP);
|
||||
topDown = false;
|
||||
} else {
|
||||
topDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
//noinspection SuspiciousNameCombination
|
||||
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
|
||||
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas){
|
||||
TextPaint textPaint = getPaint();
|
||||
textPaint.setColor(getCurrentTextColor());
|
||||
textPaint.drawableState = getDrawableState();
|
||||
|
||||
canvas.save();
|
||||
|
||||
if (topDown){
|
||||
canvas.translate(getWidth(), 0);
|
||||
canvas.rotate(90);
|
||||
} else {
|
||||
canvas.translate(0, getHeight());
|
||||
canvas.rotate(-90);
|
||||
}
|
||||
|
||||
|
||||
canvas.translate(getCompoundPaddingLeft(), getExtendedPaddingTop());
|
||||
|
||||
getLayout().draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
public class AccessibleDateAnimator extends ViewAnimator {
|
||||
private long mDateMillis;
|
||||
|
||||
public AccessibleDateAnimator(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setDateMillis(long dateMillis) {
|
||||
mDateMillis = dateMillis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce the currently-selected date when launched.
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
|
||||
// Clear the event's current text so that only the current date will be spoken.
|
||||
event.getText().clear();
|
||||
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
|
||||
DateUtils.FORMAT_SHOW_WEEKDAY;
|
||||
|
||||
String dateString = DateUtils.formatDateTime(getContext(), mDateMillis, flags);
|
||||
event.getText().add(dateString);
|
||||
return true;
|
||||
}
|
||||
return super.dispatchPopulateAccessibilityEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Controller class to communicate among the various components of the date picker dialog.
|
||||
*/
|
||||
public interface DatePickerController {
|
||||
|
||||
void onYearSelected(int year);
|
||||
|
||||
void onDayOfMonthSelected(int year, int month, int day);
|
||||
|
||||
void registerOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
void unregisterOnDateChangedListener(DatePickerDialog.OnDateChangedListener listener);
|
||||
|
||||
MonthAdapter.CalendarDay getSelectedDay();
|
||||
|
||||
boolean isThemeDark();
|
||||
|
||||
int getAccentColor();
|
||||
|
||||
boolean isHighlighted(int year, int month, int day);
|
||||
|
||||
int getFirstDayOfWeek();
|
||||
|
||||
int getMinYear();
|
||||
|
||||
int getMaxYear();
|
||||
|
||||
Calendar getStartDate();
|
||||
|
||||
Calendar getEndDate();
|
||||
|
||||
boolean isOutOfRange(int year, int month, int day);
|
||||
|
||||
void tryVibrate();
|
||||
|
||||
TimeZone getTimeZone();
|
||||
|
||||
Locale getLocale();
|
||||
|
||||
DatePickerDialog.Version getVersion();
|
||||
|
||||
DatePickerDialog.ScrollOrientation getScrollOrientation();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Wouter Dullaert
|
||||
*
|
||||
* 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.date;
|
||||
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public interface DateRangeLimiter extends Parcelable {
|
||||
/**
|
||||
* getMinYear returns the minimum selectable year of the picker.
|
||||
* This method should match getStartDate()
|
||||
* It is recommended to keep the default implementation
|
||||
* This method will be removed from this interface at the next semver major
|
||||
* @return the minimum selectable year of the picker
|
||||
*/
|
||||
default int getMinYear() {
|
||||
return getStartDate().get(Calendar.YEAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* getMaxYear returns the maximum selectable year of the picker
|
||||
* This method should semantically match getEndDate()
|
||||
* It is recommended to keep the default implementation.
|
||||
* This method will be removed from this interface at the next semver major
|
||||
* @return the maximum selectable year of the picker
|
||||
*/
|
||||
default int getMaxYear() {
|
||||
return getEndDate().get(Calendar.YEAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* getStartDate returns the minimum selectable date of the picker
|
||||
* It is called in various places, including the hot loop when rendering.
|
||||
* It is highly recommended to keep this method as simple as possible
|
||||
* @return the minimum selectable date of the picker
|
||||
*/
|
||||
@NonNull Calendar getStartDate();
|
||||
|
||||
/**
|
||||
* getEndDate returns the maximum selectable date of the picker
|
||||
* It is called in various places, including the hot loop when rendering.
|
||||
* It is highly recommended to keep this method as simple as possible
|
||||
* @return the maximum selectable date of the picker
|
||||
*/
|
||||
@NonNull Calendar getEndDate();
|
||||
|
||||
/**
|
||||
* isOutOfRange is called for each date when it is about to be rendered
|
||||
* Returning true from this function will cause that particular day to be non selectable
|
||||
* Since this code is called in the inner loop when rendering, it is highly recommended to
|
||||
* keep the logic as simple as possible
|
||||
* @param year the year of the date
|
||||
* @param month the month of the date
|
||||
* @param day the day of the month of the date
|
||||
* @return true if the date should be disabled, false otherwise
|
||||
*/
|
||||
boolean isOutOfRange(int year, int month, int day);
|
||||
|
||||
/**
|
||||
* setToNearestDate rounds a Date to the nearest selectable value.
|
||||
* It is called each time the user makes a year selection: the newly resulting date might not be
|
||||
* valid according to the constraints set by the limiter.
|
||||
* This method is not called when the user selects a day, since the picker prevents the
|
||||
* selection of values which satisfy `isOutOfRange`
|
||||
* @param day a date with the current user selection
|
||||
* @return the date after rounding to a selectable value
|
||||
*/
|
||||
@NonNull Calendar setToNearestDate(@NonNull Calendar day);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.wdullaer.materialdatetimepicker.date;
|
||||
|
||||
public enum DayOfWeek {
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.wdullaer.materialdatetimepicker.date;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.R;
|
||||
import com.wdullaer.materialdatetimepicker.Utils;
|
||||
|
||||
public class DayPickerGroup extends ViewGroup
|
||||
implements View.OnClickListener, DayPickerView.OnPageListener {
|
||||
private ImageButton prevButton;
|
||||
private ImageButton nextButton;
|
||||
private DayPickerView dayPickerView;
|
||||
private DatePickerController controller;
|
||||
|
||||
public DayPickerGroup(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public DayPickerGroup(Context context, @NonNull DatePickerController controller) {
|
||||
super(context);
|
||||
this.controller = controller;
|
||||
init();
|
||||
}
|
||||
|
||||
public DayPickerGroup(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public DayPickerGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
dayPickerView = new SimpleDayPickerView(getContext(), controller);
|
||||
addView(dayPickerView);
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
final ViewGroup content = (ViewGroup) inflater.inflate(R.layout.mdtp_daypicker_group, this, false);
|
||||
|
||||
// Transfer all children from the content to this
|
||||
while (content.getChildCount() > 0) {
|
||||
final View view = content.getChildAt(0);
|
||||
content.removeViewAt(0);
|
||||
addView(view);
|
||||
}
|
||||
|
||||
prevButton = findViewById(R.id.mdtp_previous_month_arrow);
|
||||
nextButton = findViewById(R.id.mdtp_next_month_arrow);
|
||||
|
||||
if (controller.getVersion() == DatePickerDialog.Version.VERSION_1) {
|
||||
int size = Utils.dpToPx(16f, getResources());
|
||||
prevButton.setMinimumHeight(size);
|
||||
prevButton.setMinimumWidth(size);
|
||||
nextButton.setMinimumHeight(size);
|
||||
nextButton.setMinimumWidth(size);
|
||||
}
|
||||
|
||||
if (controller.isThemeDark()) {
|
||||
int color = ContextCompat.getColor(getContext(), R.color.mdtp_date_picker_text_normal_dark_theme);
|
||||
prevButton.setColorFilter(color);
|
||||
nextButton.setColorFilter(color);
|
||||
}
|
||||
|
||||
prevButton.setOnClickListener(this);
|
||||
nextButton.setOnClickListener(this);
|
||||
|
||||
dayPickerView.setOnPageListener(this);
|
||||
}
|
||||
|
||||
private void updateButtonVisibility(int position) {
|
||||
final boolean isHorizontal = controller.getScrollOrientation() == DatePickerDialog.ScrollOrientation.HORIZONTAL;
|
||||
final boolean hasPrev = position > 0;
|
||||
final boolean hasNext = position < (dayPickerView.getCount() - 1);
|
||||
prevButton.setVisibility(isHorizontal && hasPrev ? View.VISIBLE : View.INVISIBLE);
|
||||
nextButton.setVisibility(isHorizontal && hasNext ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
public void onChange() {
|
||||
dayPickerView.onChange();
|
||||
}
|
||||
|
||||
public void onDateChanged() {
|
||||
dayPickerView.onDateChanged();
|
||||
}
|
||||
|
||||
public void postSetSelection(int position) {
|
||||
dayPickerView.postSetSelection(position);
|
||||
}
|
||||
|
||||
public int getMostVisiblePosition() {
|
||||
return dayPickerView.getMostVisiblePosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
measureChild(dayPickerView, widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
final int measuredWidthAndState = dayPickerView.getMeasuredWidthAndState();
|
||||
final int measuredHeightAndState = dayPickerView.getMeasuredHeightAndState();
|
||||
setMeasuredDimension(measuredWidthAndState, measuredHeightAndState);
|
||||
|
||||
final int pagerWidth = dayPickerView.getMeasuredWidth();
|
||||
final int pagerHeight = dayPickerView.getMeasuredHeight();
|
||||
final int buttonWidthSpec = MeasureSpec.makeMeasureSpec(pagerWidth, MeasureSpec.AT_MOST);
|
||||
final int buttonHeightSpec = MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.AT_MOST);
|
||||
prevButton.measure(buttonWidthSpec, buttonHeightSpec);
|
||||
nextButton.measure(buttonWidthSpec, buttonHeightSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
final ImageButton leftButton;
|
||||
final ImageButton rightButton;
|
||||
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
|
||||
leftButton = nextButton;
|
||||
rightButton = prevButton;
|
||||
} else {
|
||||
leftButton = prevButton;
|
||||
rightButton = nextButton;
|
||||
}
|
||||
|
||||
final int topMargin = controller.getVersion() == DatePickerDialog.Version.VERSION_1
|
||||
? 0
|
||||
: getContext().getResources().getDimensionPixelSize(R.dimen.mdtp_date_picker_view_animator_padding_v2);
|
||||
final int width = right - left;
|
||||
final int height = bottom - top;
|
||||
dayPickerView.layout(0, topMargin, width, height);
|
||||
|
||||
final SimpleMonthView monthView = (SimpleMonthView) dayPickerView.getChildAt(0);
|
||||
final int monthHeight = monthView.getMonthHeight();
|
||||
final int cellWidth = monthView.getCellWidth();
|
||||
final int edgePadding = monthView.getEdgePadding();
|
||||
|
||||
// Vertically center the previous/next buttons within the month
|
||||
// header, horizontally center within the day cell.
|
||||
final int leftDW = leftButton.getMeasuredWidth();
|
||||
final int leftDH = leftButton.getMeasuredHeight();
|
||||
final int leftIconTop = topMargin + monthView.getPaddingTop() + (monthHeight - leftDH) / 2;
|
||||
final int leftIconLeft = edgePadding + (cellWidth - leftDW) / 2;
|
||||
leftButton.layout(leftIconLeft, leftIconTop, leftIconLeft + leftDW, leftIconTop + leftDH);
|
||||
|
||||
final int rightDW = rightButton.getMeasuredWidth();
|
||||
final int rightDH = rightButton.getMeasuredHeight();
|
||||
final int rightIconTop = topMargin + monthView.getPaddingTop() + (monthHeight - rightDH) / 2;
|
||||
final int rightIconRight = width - edgePadding - (cellWidth - rightDW) / 2 - 2;
|
||||
rightButton.layout(rightIconRight - rightDW, rightIconTop,
|
||||
rightIconRight, rightIconTop + rightDH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageChanged(int position) {
|
||||
updateButtonVisibility(position);
|
||||
dayPickerView.accessibilityAnnouncePageChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View v) {
|
||||
int offset;
|
||||
if (nextButton == v) {
|
||||
offset = 1;
|
||||
} else if (prevButton == v) {
|
||||
offset = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
int position = dayPickerView.getMostVisiblePosition() + offset;
|
||||
|
||||
// updateButtonVisibility only triggers when a scroll is completed. So a user might
|
||||
// click the button when the animation is still ongoing potentially pushing the target
|
||||
// position outside of the bounds of the dayPickerView
|
||||
if (position >= 0 && position < dayPickerView.getCount()) {
|
||||
dayPickerView.smoothScrollToPosition(position);
|
||||
updateButtonVisibility(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.GravitySnapHelper;
|
||||
import com.wdullaer.materialdatetimepicker.Utils;
|
||||
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog.OnDateChangedListener;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* This displays a list of months in a calendar format with selectable days.
|
||||
*/
|
||||
public abstract class DayPickerView extends RecyclerView implements OnDateChangedListener {
|
||||
|
||||
private static final String TAG = "MonthFragment";
|
||||
|
||||
protected Context mContext;
|
||||
|
||||
// highlighted time
|
||||
protected MonthAdapter.CalendarDay mSelectedDay;
|
||||
protected MonthAdapter mAdapter;
|
||||
|
||||
protected MonthAdapter.CalendarDay mTempDay;
|
||||
|
||||
// which month should be displayed/highlighted [0-11]
|
||||
protected int mCurrentMonthDisplayed;
|
||||
// used for tracking what state listview is in
|
||||
protected int mPreviousScrollState = RecyclerView.SCROLL_STATE_IDLE;
|
||||
|
||||
private OnPageListener pageListener;
|
||||
private DatePickerController mController;
|
||||
|
||||
public interface OnPageListener {
|
||||
/**
|
||||
* Called when the visible page of the DayPickerView has changed
|
||||
* @param position the new position visible in the DayPickerView
|
||||
*/
|
||||
void onPageChanged(int position);
|
||||
}
|
||||
|
||||
public DayPickerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
DatePickerDialog.ScrollOrientation scrollOrientation = Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|
||||
? DatePickerDialog.ScrollOrientation.VERTICAL
|
||||
: DatePickerDialog.ScrollOrientation.HORIZONTAL;
|
||||
init(context, scrollOrientation);
|
||||
}
|
||||
|
||||
public DayPickerView(Context context, DatePickerController controller) {
|
||||
super(context);
|
||||
init(context, controller.getScrollOrientation());
|
||||
setController(controller);
|
||||
}
|
||||
|
||||
protected void setController(DatePickerController controller) {
|
||||
mController = controller;
|
||||
mController.registerOnDateChangedListener(this);
|
||||
mSelectedDay = new MonthAdapter.CalendarDay(mController.getTimeZone());
|
||||
mTempDay = new MonthAdapter.CalendarDay(mController.getTimeZone());
|
||||
refreshAdapter();
|
||||
}
|
||||
|
||||
public void init(Context context, DatePickerDialog.ScrollOrientation scrollOrientation) {
|
||||
@RecyclerView.Orientation
|
||||
int layoutOrientation = scrollOrientation == DatePickerDialog.ScrollOrientation.VERTICAL
|
||||
? RecyclerView.VERTICAL
|
||||
: RecyclerView.HORIZONTAL;
|
||||
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context, layoutOrientation, false);
|
||||
setLayoutManager(linearLayoutManager);
|
||||
setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
setVerticalScrollBarEnabled(false);
|
||||
setHorizontalScrollBarEnabled(false);
|
||||
setClipChildren(false);
|
||||
|
||||
mContext = context;
|
||||
setUpRecyclerView(scrollOrientation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all the required fields for the list view. Override this method to
|
||||
* set a different list view behavior.
|
||||
*/
|
||||
protected void setUpRecyclerView(DatePickerDialog.ScrollOrientation scrollOrientation) {
|
||||
setVerticalScrollBarEnabled(false);
|
||||
setFadingEdgeLength(0);
|
||||
int gravity = scrollOrientation == DatePickerDialog.ScrollOrientation.VERTICAL
|
||||
? Gravity.TOP
|
||||
: Gravity.START;
|
||||
GravitySnapHelper helper = new GravitySnapHelper(gravity, position -> {
|
||||
// Leverage the fact that the SnapHelper figures out which position is shown and
|
||||
// pass this on to our PageListener after the snap has happened
|
||||
if (pageListener != null) pageListener.onPageChanged(position);
|
||||
});
|
||||
helper.attachToRecyclerView(this);
|
||||
}
|
||||
|
||||
public void onChange() {
|
||||
refreshAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
final MonthAdapter.CalendarDay focusedDay = findAccessibilityFocus();
|
||||
restoreAccessibilityFocus(focusedDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new adapter if necessary and sets up its parameters. Override
|
||||
* this method to provide a custom adapter.
|
||||
*/
|
||||
protected void refreshAdapter() {
|
||||
if (mAdapter == null) {
|
||||
mAdapter = createMonthAdapter(mController);
|
||||
} else {
|
||||
mAdapter.setSelectedDay(mSelectedDay);
|
||||
if (pageListener != null) pageListener.onPageChanged(getMostVisiblePosition());
|
||||
}
|
||||
// refresh the view with the new parameters
|
||||
setAdapter(mAdapter);
|
||||
}
|
||||
|
||||
public abstract MonthAdapter createMonthAdapter(DatePickerController controller);
|
||||
|
||||
public void setOnPageListener(@Nullable OnPageListener pageListener) {
|
||||
this.pageListener = pageListener;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unused")
|
||||
public OnPageListener getOnPageListener() {
|
||||
return pageListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* This moves to the specified time in the view. If the time is not already
|
||||
* in range it will move the list so that the first of the month containing
|
||||
* the time is at the top of the view. If the new time is already in view
|
||||
* the list will not be scrolled unless forceScroll is true. This time may
|
||||
* optionally be highlighted as selected as well.
|
||||
*
|
||||
* @param day The day to move to
|
||||
* @param animate Whether to scroll to the given time or just redraw at the
|
||||
* new location
|
||||
* @param setSelected Whether to set the given time as selected
|
||||
* @param forceScroll Whether to recenter even if the time is already
|
||||
* visible
|
||||
* @return Whether or not the view animated to the new location
|
||||
*/
|
||||
public boolean goTo(MonthAdapter.CalendarDay day, boolean animate, boolean setSelected, boolean forceScroll) {
|
||||
|
||||
// Set the selected day
|
||||
if (setSelected) {
|
||||
mSelectedDay.set(day);
|
||||
}
|
||||
|
||||
mTempDay.set(day);
|
||||
int minMonth = mController.getStartDate().get(Calendar.MONTH);
|
||||
final int position = (day.year - mController.getMinYear())
|
||||
* MonthAdapter.MONTHS_IN_YEAR + day.month - minMonth;
|
||||
|
||||
View child;
|
||||
int i = 0;
|
||||
int top = 0;
|
||||
// Find a child that's completely in the view
|
||||
do {
|
||||
child = getChildAt(i++);
|
||||
if (child == null) {
|
||||
break;
|
||||
}
|
||||
top = child.getTop();
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "child at " + (i - 1) + " has top " + top);
|
||||
}
|
||||
} while (top < 0);
|
||||
|
||||
// Compute the first and last position visible
|
||||
int selectedPosition = child != null ? getChildAdapterPosition(child) : 0;
|
||||
|
||||
if (setSelected) {
|
||||
mAdapter.setSelectedDay(mSelectedDay);
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "GoTo position " + position);
|
||||
}
|
||||
// Check if the selected day is now outside of our visible range
|
||||
// and if so scroll to the month that contains it
|
||||
if (position != selectedPosition || forceScroll) {
|
||||
setMonthDisplayed(mTempDay);
|
||||
mPreviousScrollState = RecyclerView.SCROLL_STATE_DRAGGING;
|
||||
if (animate) {
|
||||
smoothScrollToPosition(position);
|
||||
if (pageListener != null) pageListener.onPageChanged(position);
|
||||
return true;
|
||||
} else {
|
||||
postSetSelection(position);
|
||||
}
|
||||
} else if (setSelected) {
|
||||
setMonthDisplayed(mSelectedDay);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void postSetSelection(final int position) {
|
||||
clearFocus();
|
||||
post(() -> {
|
||||
((LinearLayoutManager) getLayoutManager()).scrollToPositionWithOffset(position, 0);
|
||||
|
||||
// Set initial accessibility focus to selected day
|
||||
restoreAccessibilityFocus(mSelectedDay);
|
||||
|
||||
if (pageListener != null) pageListener.onPageChanged(position);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the month displayed at the top of this view based on time. Override
|
||||
* to add custom events when the title is changed.
|
||||
*/
|
||||
protected void setMonthDisplayed(MonthAdapter.CalendarDay date) {
|
||||
mCurrentMonthDisplayed = date.month;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the position of the view that is most prominently displayed within the list.
|
||||
*/
|
||||
public int getMostVisiblePosition() {
|
||||
return getChildAdapterPosition(getMostVisibleMonth());
|
||||
}
|
||||
|
||||
public @Nullable MonthView getMostVisibleMonth() {
|
||||
boolean verticalScroll = mController.getScrollOrientation() == DatePickerDialog.ScrollOrientation.VERTICAL;
|
||||
final int maxSize = verticalScroll ? getHeight() : getWidth();
|
||||
int maxDisplayedSize = 0;
|
||||
int i = 0;
|
||||
int size = 0;
|
||||
MonthView mostVisibleMonth = null;
|
||||
|
||||
while (size < maxSize) {
|
||||
View child = getChildAt(i);
|
||||
if (child == null) {
|
||||
break;
|
||||
}
|
||||
size = verticalScroll ? child.getBottom() : child.getRight();
|
||||
int endPosition = verticalScroll ? child.getTop() : child.getLeft();
|
||||
int displayedSize = Math.min(size, maxSize) - Math.max(0, endPosition);
|
||||
if (displayedSize > maxDisplayedSize) {
|
||||
mostVisibleMonth = (MonthView) child;
|
||||
maxDisplayedSize = displayedSize;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return mostVisibleMonth;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return mAdapter.getItemCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* This should only be called when the DayPickerView is visible, or when it has already been
|
||||
* requested to be visible
|
||||
*/
|
||||
@Override
|
||||
public void onDateChanged() {
|
||||
goTo(mController.getSelectedDay(), false, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to return the date that has accessibility focus.
|
||||
*
|
||||
* @return The date that has accessibility focus, or {@code null} if no date
|
||||
* has focus.
|
||||
*/
|
||||
private MonthAdapter.CalendarDay findAccessibilityFocus() {
|
||||
final int childCount = getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
if (child instanceof MonthView) {
|
||||
final MonthAdapter.CalendarDay focus = ((MonthView) child).getAccessibilityFocus();
|
||||
if (focus != null) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
// Clear focus to avoid ListView bug in Jelly Bean MR1.
|
||||
((MonthView) child).clearAccessibilityFocus();
|
||||
}
|
||||
return focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to restore accessibility focus to a given date. No-op if
|
||||
* {@code day} is {@code null}.
|
||||
*
|
||||
* @param day The date that should receive accessibility focus
|
||||
* @return {@code true} if focus was restored
|
||||
*/
|
||||
private boolean restoreAccessibilityFocus(MonthAdapter.CalendarDay day) {
|
||||
if (day == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int childCount = getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
if (child instanceof MonthView) {
|
||||
if (((MonthView) child).restoreAccessibilityFocus(day)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
event.setItemCount(-1);
|
||||
}
|
||||
|
||||
void accessibilityAnnouncePageChanged() {
|
||||
MonthView mv = getMostVisibleMonth();
|
||||
if (mv != null) {
|
||||
String monthYear = getMonthAndYearString(mv.mMonth, mv.mYear, mController.getLocale());
|
||||
Utils.tryAccessibilityAnnounce(this, monthYear);
|
||||
} else {
|
||||
Log.w("DayPickerView", "Tried to announce before layout was initialized");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getMonthAndYearString(int month, int year, Locale locale) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.MONTH, month);
|
||||
calendar.set(Calendar.YEAR, year);
|
||||
return new SimpleDateFormat("MMMM yyyy", locale).format(calendar.getTime());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Wouter Dullaert
|
||||
*
|
||||
* 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.date;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.Utils;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.TimeZone;
|
||||
import java.util.TreeSet;
|
||||
|
||||
class DefaultDateRangeLimiter implements DateRangeLimiter {
|
||||
private static final int DEFAULT_START_YEAR = 1900;
|
||||
private static final int DEFAULT_END_YEAR = 2100;
|
||||
|
||||
private transient DatePickerController mController;
|
||||
private int mMinYear = DEFAULT_START_YEAR;
|
||||
private int mMaxYear = DEFAULT_END_YEAR;
|
||||
private Calendar mMinDate;
|
||||
private Calendar mMaxDate;
|
||||
private TreeSet<Calendar> selectableDays = new TreeSet<>();
|
||||
private HashSet<Calendar> disabledDays = new HashSet<>();
|
||||
|
||||
DefaultDateRangeLimiter() {}
|
||||
|
||||
@SuppressWarnings({"unchecked", "WeakerAccess"})
|
||||
public DefaultDateRangeLimiter(Parcel in) {
|
||||
mMinYear = in.readInt();
|
||||
mMaxYear = in.readInt();
|
||||
mMinDate = (Calendar) in.readSerializable();
|
||||
mMaxDate = (Calendar) in.readSerializable();
|
||||
selectableDays = (TreeSet<Calendar>) in.readSerializable();
|
||||
disabledDays = (HashSet<Calendar>) in.readSerializable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(mMinYear);
|
||||
out.writeInt(mMaxYear);
|
||||
out.writeSerializable(mMinDate);
|
||||
out.writeSerializable(mMaxDate);
|
||||
out.writeSerializable(selectableDays);
|
||||
out.writeSerializable(disabledDays);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static final Parcelable.Creator<DefaultDateRangeLimiter> CREATOR
|
||||
= new Parcelable.Creator<DefaultDateRangeLimiter>() {
|
||||
public DefaultDateRangeLimiter createFromParcel(Parcel in) {
|
||||
return new DefaultDateRangeLimiter(in);
|
||||
}
|
||||
|
||||
public DefaultDateRangeLimiter[] newArray(int size) {
|
||||
return new DefaultDateRangeLimiter[size];
|
||||
}
|
||||
};
|
||||
|
||||
void setSelectableDays(@NonNull Calendar[] days) {
|
||||
for (Calendar selectableDay : days) {
|
||||
this.selectableDays.add(Utils.trimToMidnight((Calendar) selectableDay.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
void setDisabledDays(@NonNull Calendar[] days) {
|
||||
for (Calendar disabledDay : days) {
|
||||
this.disabledDays.add(Utils.trimToMidnight((Calendar) disabledDay.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
void setMinDate(@NonNull Calendar calendar) {
|
||||
mMinDate = Utils.trimToMidnight((Calendar) calendar.clone());
|
||||
}
|
||||
|
||||
void setMaxDate(@NonNull Calendar calendar) {
|
||||
mMaxDate = Utils.trimToMidnight((Calendar) calendar.clone());
|
||||
}
|
||||
|
||||
void setController(@NonNull DatePickerController controller) {
|
||||
mController = controller;
|
||||
}
|
||||
|
||||
void setYearRange(int startYear, int endYear) {
|
||||
if (endYear < startYear) {
|
||||
throw new IllegalArgumentException("Year end must be larger than or equal to year start");
|
||||
}
|
||||
|
||||
mMinYear = startYear;
|
||||
mMaxYear = endYear;
|
||||
}
|
||||
|
||||
@Nullable Calendar getMinDate() {
|
||||
return mMinDate;
|
||||
}
|
||||
|
||||
@Nullable Calendar getMaxDate() {
|
||||
return mMaxDate;
|
||||
}
|
||||
|
||||
@Nullable Calendar[] getSelectableDays() {
|
||||
return selectableDays.isEmpty() ? null : selectableDays.toArray(new Calendar[0]);
|
||||
}
|
||||
|
||||
@Nullable Calendar[] getDisabledDays() {
|
||||
return disabledDays.isEmpty() ? null : disabledDays.toArray(new Calendar[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinYear() {
|
||||
if (!selectableDays.isEmpty()) return selectableDays.first().get(Calendar.YEAR);
|
||||
// Ensure no years can be selected outside of the given minimum date
|
||||
return mMinDate != null && mMinDate.get(Calendar.YEAR) > mMinYear ? mMinDate.get(Calendar.YEAR) : mMinYear;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxYear() {
|
||||
if (!selectableDays.isEmpty()) return selectableDays.last().get(Calendar.YEAR);
|
||||
// Ensure no years can be selected outside of the given maximum date
|
||||
return mMaxDate != null && mMaxDate.get(Calendar.YEAR) < mMaxYear ? mMaxDate.get(Calendar.YEAR) : mMaxYear;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Calendar getStartDate() {
|
||||
if (!selectableDays.isEmpty()) return (Calendar) selectableDays.first().clone();
|
||||
if (mMinDate != null) return (Calendar) mMinDate.clone();
|
||||
TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone();
|
||||
Calendar output = Calendar.getInstance(timeZone);
|
||||
output.set(Calendar.YEAR, mMinYear);
|
||||
output.set(Calendar.DAY_OF_MONTH, 1);
|
||||
output.set(Calendar.MONTH, Calendar.JANUARY);
|
||||
return output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Calendar getEndDate() {
|
||||
if (!selectableDays.isEmpty()) return (Calendar) selectableDays.last().clone();
|
||||
if (mMaxDate != null) return (Calendar) mMaxDate.clone();
|
||||
TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone();
|
||||
Calendar output = Calendar.getInstance(timeZone);
|
||||
output.set(Calendar.YEAR, mMaxYear);
|
||||
output.set(Calendar.DAY_OF_MONTH, 31);
|
||||
output.set(Calendar.MONTH, Calendar.DECEMBER);
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the specified year/month/day are within the selectable days or the range set by minDate and maxDate.
|
||||
* If one or either have not been set, they are considered as Integer.MIN_VALUE and
|
||||
* Integer.MAX_VALUE.
|
||||
*/
|
||||
@Override
|
||||
public boolean isOutOfRange(int year, int month, int day) {
|
||||
TimeZone timezone = mController == null ? TimeZone.getDefault() : mController.getTimeZone();
|
||||
Calendar date = Calendar.getInstance(timezone);
|
||||
date.set(Calendar.YEAR, year);
|
||||
date.set(Calendar.MONTH, month);
|
||||
date.set(Calendar.DAY_OF_MONTH, day);
|
||||
return isOutOfRange(date);
|
||||
}
|
||||
|
||||
private boolean isOutOfRange(@NonNull Calendar calendar) {
|
||||
Utils.trimToMidnight(calendar);
|
||||
return isDisabled(calendar) || !isSelectable(calendar);
|
||||
}
|
||||
|
||||
private boolean isDisabled(@NonNull Calendar c) {
|
||||
return disabledDays.contains(Utils.trimToMidnight(c)) || isBeforeMin(c) || isAfterMax(c);
|
||||
}
|
||||
|
||||
private boolean isSelectable(@NonNull Calendar c) {
|
||||
return selectableDays.isEmpty() || selectableDays.contains(Utils.trimToMidnight(c));
|
||||
}
|
||||
|
||||
private boolean isBeforeMin(@NonNull Calendar calendar) {
|
||||
return mMinDate != null && calendar.before(mMinDate) || calendar.get(Calendar.YEAR) < mMinYear;
|
||||
}
|
||||
|
||||
private boolean isAfterMax(@NonNull Calendar calendar) {
|
||||
return mMaxDate != null && calendar.after(mMaxDate) || calendar.get(Calendar.YEAR) > mMaxYear;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Calendar setToNearestDate(@NonNull Calendar calendar) {
|
||||
if (!selectableDays.isEmpty()) {
|
||||
Calendar newCalendar = null;
|
||||
Calendar higher = selectableDays.ceiling(calendar);
|
||||
Calendar lower = selectableDays.lower(calendar);
|
||||
|
||||
if (higher == null && lower != null) newCalendar = lower;
|
||||
else if (lower == null && higher != null) newCalendar = higher;
|
||||
|
||||
if (newCalendar != null || higher == null) {
|
||||
newCalendar = newCalendar == null ? calendar : newCalendar;
|
||||
TimeZone timeZone = mController == null ? TimeZone.getDefault() : mController.getTimeZone();
|
||||
newCalendar.setTimeZone(timeZone);
|
||||
return (Calendar) newCalendar.clone();
|
||||
}
|
||||
|
||||
long highDistance = Math.abs(higher.getTimeInMillis() - calendar.getTimeInMillis());
|
||||
long lowDistance = Math.abs(calendar.getTimeInMillis() - lower.getTimeInMillis());
|
||||
|
||||
if (lowDistance < highDistance) return (Calendar) lower.clone();
|
||||
else return (Calendar) higher.clone();
|
||||
}
|
||||
|
||||
if (!disabledDays.isEmpty()) {
|
||||
Calendar forwardDate = isBeforeMin(calendar) ? getStartDate() : (Calendar) calendar.clone();
|
||||
Calendar backwardDate = isAfterMax(calendar) ? getEndDate() : (Calendar) calendar.clone();
|
||||
while (isDisabled(forwardDate) && isDisabled(backwardDate)) {
|
||||
forwardDate.add(Calendar.DAY_OF_MONTH, 1);
|
||||
backwardDate.add(Calendar.DAY_OF_MONTH, -1);
|
||||
}
|
||||
if (!isDisabled(backwardDate)) {
|
||||
return backwardDate;
|
||||
}
|
||||
if (!isDisabled(forwardDate)) {
|
||||
return forwardDate;
|
||||
}
|
||||
}
|
||||
|
||||
TimeZone timezone = mController == null ? TimeZone.getDefault() : mController.getTimeZone();
|
||||
if (isBeforeMin(calendar)) {
|
||||
if (mMinDate != null) return (Calendar) mMinDate.clone();
|
||||
Calendar output = Calendar.getInstance(timezone);
|
||||
output.set(Calendar.YEAR, mMinYear);
|
||||
output.set(Calendar.MONTH, Calendar.JANUARY);
|
||||
output.set(Calendar.DAY_OF_MONTH, 1);
|
||||
return Utils.trimToMidnight(output);
|
||||
}
|
||||
|
||||
if (isAfterMax(calendar)) {
|
||||
if (mMaxDate != null) return (Calendar) mMaxDate.clone();
|
||||
Calendar output = Calendar.getInstance(timezone);
|
||||
output.set(Calendar.YEAR, mMaxYear);
|
||||
output.set(Calendar.MONTH, Calendar.DECEMBER);
|
||||
output.set(Calendar.DAY_OF_MONTH, 31);
|
||||
return Utils.trimToMidnight(output);
|
||||
}
|
||||
|
||||
return calendar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView.LayoutParams;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.date.MonthAdapter.MonthViewHolder;
|
||||
import com.wdullaer.materialdatetimepicker.date.MonthView.OnDayClickListener;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* An adapter for a list of {@link MonthView} items.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public abstract class MonthAdapter extends RecyclerView.Adapter<MonthViewHolder> implements OnDayClickListener {
|
||||
|
||||
protected final DatePickerController mController;
|
||||
|
||||
private CalendarDay mSelectedDay;
|
||||
|
||||
protected static final int MONTHS_IN_YEAR = 12;
|
||||
|
||||
/**
|
||||
* A convenience class to represent a specific date.
|
||||
*/
|
||||
public static class CalendarDay {
|
||||
private Calendar calendar;
|
||||
int year;
|
||||
int month;
|
||||
int day;
|
||||
TimeZone mTimeZone;
|
||||
|
||||
public CalendarDay(TimeZone timeZone) {
|
||||
mTimeZone = timeZone;
|
||||
setTime(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public CalendarDay(long timeInMillis, TimeZone timeZone) {
|
||||
mTimeZone = timeZone;
|
||||
setTime(timeInMillis);
|
||||
}
|
||||
|
||||
public CalendarDay(Calendar calendar, TimeZone timeZone) {
|
||||
mTimeZone = timeZone;
|
||||
year = calendar.get(Calendar.YEAR);
|
||||
month = calendar.get(Calendar.MONTH);
|
||||
day = calendar.get(Calendar.DAY_OF_MONTH);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public CalendarDay(int year, int month, int day) {
|
||||
setDay(year, month, day);
|
||||
}
|
||||
|
||||
public CalendarDay(int year, int month, int day, TimeZone timezone) {
|
||||
mTimeZone = timezone;
|
||||
setDay(year, month, day);
|
||||
}
|
||||
|
||||
public void set(CalendarDay date) {
|
||||
year = date.year;
|
||||
month = date.month;
|
||||
day = date.day;
|
||||
}
|
||||
|
||||
public void setDay(int year, int month, int day) {
|
||||
this.year = year;
|
||||
this.month = month;
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
private void setTime(long timeInMillis) {
|
||||
if (calendar == null) {
|
||||
calendar = Calendar.getInstance(mTimeZone);
|
||||
}
|
||||
calendar.setTimeInMillis(timeInMillis);
|
||||
month = calendar.get(Calendar.MONTH);
|
||||
year = calendar.get(Calendar.YEAR);
|
||||
day = calendar.get(Calendar.DAY_OF_MONTH);
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return year;
|
||||
}
|
||||
|
||||
public int getMonth() {
|
||||
return month;
|
||||
}
|
||||
|
||||
public int getDay() {
|
||||
return day;
|
||||
}
|
||||
}
|
||||
|
||||
public MonthAdapter(DatePickerController controller) {
|
||||
mController = controller;
|
||||
init();
|
||||
setSelectedDay(mController.getSelectedDay());
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the selected day and related parameters.
|
||||
*
|
||||
* @param day The day to highlight
|
||||
*/
|
||||
public void setSelectedDay(CalendarDay day) {
|
||||
mSelectedDay = day;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public CalendarDay getSelectedDay() {
|
||||
return mSelectedDay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the gesture detector and selected time
|
||||
*/
|
||||
protected void init() {
|
||||
mSelectedDay = new CalendarDay(System.currentTimeMillis(), mController.getTimeZone());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public MonthViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
|
||||
MonthView v = createMonthView(parent.getContext());
|
||||
// Set up the new view
|
||||
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
v.setLayoutParams(params);
|
||||
v.setClickable(true);
|
||||
v.setOnDayClickListener(this);
|
||||
|
||||
return new MonthViewHolder(v);
|
||||
}
|
||||
|
||||
@Override public void onBindViewHolder(@NonNull MonthViewHolder holder, int position) {
|
||||
holder.bind(position, mController, mSelectedDay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override public int getItemCount() {
|
||||
Calendar endDate = mController.getEndDate();
|
||||
Calendar startDate = mController.getStartDate();
|
||||
int endMonth = endDate.get(Calendar.YEAR) * MONTHS_IN_YEAR + endDate.get(Calendar.MONTH);
|
||||
int startMonth = startDate.get(Calendar.YEAR) * MONTHS_IN_YEAR + startDate.get(Calendar.MONTH);
|
||||
return endMonth - startMonth + 1;
|
||||
}
|
||||
|
||||
public abstract MonthView createMonthView(Context context);
|
||||
|
||||
@Override
|
||||
public void onDayClick(MonthView view, CalendarDay day) {
|
||||
if (day != null) {
|
||||
onDayTapped(day);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains the same hour/min/sec but moves the day to the tapped day.
|
||||
*
|
||||
* @param day The day that was tapped
|
||||
*/
|
||||
protected void onDayTapped(CalendarDay day) {
|
||||
mController.tryVibrate();
|
||||
mController.onDayOfMonthSelected(day.year, day.month, day.day);
|
||||
setSelectedDay(day);
|
||||
}
|
||||
|
||||
static class MonthViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public MonthViewHolder(MonthView itemView) {
|
||||
super(itemView);
|
||||
|
||||
}
|
||||
|
||||
void bind(int position, DatePickerController mController, CalendarDay selectedCalendarDay) {
|
||||
final int month = (position + mController.getStartDate().get(Calendar.MONTH)) % MONTHS_IN_YEAR;
|
||||
final int year = (position + mController.getStartDate().get(Calendar.MONTH)) / MONTHS_IN_YEAR + mController.getMinYear();
|
||||
|
||||
int selectedDay = -1;
|
||||
if (isSelectedDayInMonth(selectedCalendarDay, year, month)) {
|
||||
selectedDay = selectedCalendarDay.day;
|
||||
}
|
||||
|
||||
((MonthView) itemView).setMonthParams(selectedDay, year, month, mController.getFirstDayOfWeek());
|
||||
this.itemView.invalidate();
|
||||
}
|
||||
|
||||
private boolean isSelectedDayInMonth(CalendarDay selectedDay, int year, int month) {
|
||||
return selectedDay.year == year && selectedDay.month == month;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,769 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.customview.widget.ExploreByTouchHelper;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.R;
|
||||
import com.wdullaer.materialdatetimepicker.date.MonthAdapter.CalendarDay;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A calendar-like view displaying a specified month and the appropriate selectable day numbers
|
||||
* within the specified month.
|
||||
*/
|
||||
public abstract class MonthView extends View {
|
||||
|
||||
protected static int DEFAULT_HEIGHT = 32;
|
||||
protected static final int DEFAULT_SELECTED_DAY = -1;
|
||||
protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
|
||||
protected static final int DEFAULT_NUM_DAYS = 7;
|
||||
protected static final int DEFAULT_NUM_ROWS = 6;
|
||||
protected static final int MAX_NUM_ROWS = 6;
|
||||
|
||||
private static final int SELECTED_CIRCLE_ALPHA = 255;
|
||||
|
||||
protected static int DAY_SEPARATOR_WIDTH = 1;
|
||||
protected static int MINI_DAY_NUMBER_TEXT_SIZE;
|
||||
protected static int MONTH_LABEL_TEXT_SIZE;
|
||||
protected static int MONTH_DAY_LABEL_TEXT_SIZE;
|
||||
protected static int MONTH_HEADER_SIZE;
|
||||
protected static int MONTH_HEADER_SIZE_V2;
|
||||
protected static int DAY_SELECTED_CIRCLE_SIZE;
|
||||
protected static int DAY_HIGHLIGHT_CIRCLE_SIZE;
|
||||
protected static int DAY_HIGHLIGHT_CIRCLE_MARGIN;
|
||||
|
||||
protected DatePickerController mController;
|
||||
|
||||
// affects the padding on the sides of this view
|
||||
protected int mEdgePadding = 0;
|
||||
|
||||
private String mDayOfWeekTypeface;
|
||||
private String mMonthTitleTypeface;
|
||||
|
||||
protected Paint mMonthNumPaint;
|
||||
protected Paint mMonthTitlePaint;
|
||||
protected Paint mSelectedCirclePaint;
|
||||
protected Paint mMonthDayLabelPaint;
|
||||
|
||||
private final StringBuilder mStringBuilder;
|
||||
|
||||
protected int mMonth;
|
||||
|
||||
protected int mYear;
|
||||
// Quick reference to the width of this view, matches parent
|
||||
protected int mWidth;
|
||||
// The height this view should draw at in pixels, set by height param
|
||||
protected int mRowHeight = DEFAULT_HEIGHT;
|
||||
// If this view contains the today
|
||||
protected boolean mHasToday = false;
|
||||
// Which day is selected [0-6] or -1 if no day is selected
|
||||
protected int mSelectedDay = -1;
|
||||
// Which day is today [0-6] or -1 if no day is today
|
||||
protected int mToday = DEFAULT_SELECTED_DAY;
|
||||
// Which day of the week to start on [0-6]
|
||||
protected int mWeekStart = DEFAULT_WEEK_START;
|
||||
// How many days to display
|
||||
protected int mNumDays = DEFAULT_NUM_DAYS;
|
||||
// The number of days + a spot for week number if it is displayed
|
||||
protected int mNumCells = mNumDays;
|
||||
|
||||
private final Calendar mCalendar;
|
||||
protected final Calendar mDayLabelCalendar;
|
||||
private final MonthViewTouchHelper mTouchHelper;
|
||||
|
||||
protected int mNumRows = DEFAULT_NUM_ROWS;
|
||||
|
||||
// Optional listener for handling day click actions
|
||||
protected OnDayClickListener mOnDayClickListener;
|
||||
|
||||
// Whether to prevent setting the accessibility delegate
|
||||
private boolean mLockAccessibilityDelegate;
|
||||
|
||||
protected int mDayTextColor;
|
||||
protected int mSelectedDayTextColor;
|
||||
protected int mMonthDayTextColor;
|
||||
protected int mTodayNumberColor;
|
||||
protected int mHighlightedDayTextColor;
|
||||
protected int mDisabledDayTextColor;
|
||||
protected int mMonthTitleColor;
|
||||
|
||||
private SimpleDateFormat weekDayLabelFormatter;
|
||||
|
||||
public MonthView(Context context) {
|
||||
this(context, null, null);
|
||||
}
|
||||
|
||||
public MonthView(Context context, AttributeSet attr, DatePickerController controller) {
|
||||
super(context, attr);
|
||||
mController = controller;
|
||||
Resources res = context.getResources();
|
||||
|
||||
mDayLabelCalendar = Calendar.getInstance(mController.getTimeZone(), mController.getLocale());
|
||||
mCalendar = Calendar.getInstance(mController.getTimeZone(), mController.getLocale());
|
||||
|
||||
mDayOfWeekTypeface = res.getString(R.string.mdtp_day_of_week_label_typeface);
|
||||
mMonthTitleTypeface = res.getString(R.string.mdtp_sans_serif);
|
||||
|
||||
boolean darkTheme = mController != null && mController.isThemeDark();
|
||||
if (darkTheme) {
|
||||
mDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_normal_dark_theme);
|
||||
mMonthDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_month_day_dark_theme);
|
||||
mDisabledDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_disabled_dark_theme);
|
||||
mHighlightedDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_highlighted_dark_theme);
|
||||
} else {
|
||||
mDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_normal);
|
||||
mMonthDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_month_day);
|
||||
mDisabledDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_disabled);
|
||||
mHighlightedDayTextColor = ContextCompat.getColor(context, R.color.mdtp_date_picker_text_highlighted);
|
||||
}
|
||||
mSelectedDayTextColor = ContextCompat.getColor(context, R.color.mdtp_white);
|
||||
mTodayNumberColor = mController.getAccentColor();
|
||||
mMonthTitleColor = ContextCompat.getColor(context, R.color.mdtp_white);
|
||||
|
||||
mStringBuilder = new StringBuilder(50);
|
||||
|
||||
MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_day_number_size);
|
||||
MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_label_size);
|
||||
MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.mdtp_month_day_label_text_size);
|
||||
MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.mdtp_month_list_item_header_height);
|
||||
MONTH_HEADER_SIZE_V2 = res.getDimensionPixelOffset(R.dimen.mdtp_month_list_item_header_height_v2);
|
||||
DAY_SELECTED_CIRCLE_SIZE = mController.getVersion() == DatePickerDialog.Version.VERSION_1
|
||||
? res.getDimensionPixelSize(R.dimen.mdtp_day_number_select_circle_radius)
|
||||
: res.getDimensionPixelSize(R.dimen.mdtp_day_number_select_circle_radius_v2);
|
||||
DAY_HIGHLIGHT_CIRCLE_SIZE = res
|
||||
.getDimensionPixelSize(R.dimen.mdtp_day_highlight_circle_radius);
|
||||
DAY_HIGHLIGHT_CIRCLE_MARGIN = res
|
||||
.getDimensionPixelSize(R.dimen.mdtp_day_highlight_circle_margin);
|
||||
|
||||
if (mController.getVersion() == DatePickerDialog.Version.VERSION_1) {
|
||||
mRowHeight = (res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height)
|
||||
- getMonthHeaderSize()) / MAX_NUM_ROWS;
|
||||
} else {
|
||||
mRowHeight = (res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height_v2)
|
||||
- getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE * 2) / MAX_NUM_ROWS;
|
||||
}
|
||||
|
||||
mEdgePadding = mController.getVersion() == DatePickerDialog.Version.VERSION_1
|
||||
? 0
|
||||
: context.getResources().getDimensionPixelSize(R.dimen.mdtp_date_picker_view_animator_padding_v2);
|
||||
|
||||
// Set up accessibility components.
|
||||
mTouchHelper = getMonthViewTouchHelper();
|
||||
ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
|
||||
ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
mLockAccessibilityDelegate = true;
|
||||
|
||||
// Sets up any standard paints that will be used
|
||||
initView();
|
||||
}
|
||||
|
||||
protected MonthViewTouchHelper getMonthViewTouchHelper() {
|
||||
return new MonthViewTouchHelper(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
|
||||
// Workaround for a JB MR1 issue where accessibility delegates on
|
||||
// top-level ListView items are overwritten.
|
||||
if (!mLockAccessibilityDelegate) {
|
||||
super.setAccessibilityDelegate(delegate);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnDayClickListener(OnDayClickListener listener) {
|
||||
mOnDayClickListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchHoverEvent(@NonNull MotionEvent event) {
|
||||
// First right-of-refusal goes the touch exploration helper.
|
||||
return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_UP:
|
||||
final int day = getDayFromLocation(event.getX(), event.getY());
|
||||
if (day >= 0) {
|
||||
onDayClick(day);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the text and style properties for painting. Override this if you
|
||||
* want to use a different paint.
|
||||
*/
|
||||
protected void initView() {
|
||||
mMonthTitlePaint = new Paint();
|
||||
if (mController.getVersion() == DatePickerDialog.Version.VERSION_1)
|
||||
mMonthTitlePaint.setFakeBoldText(true);
|
||||
mMonthTitlePaint.setAntiAlias(true);
|
||||
mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
|
||||
mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
|
||||
mMonthTitlePaint.setColor(mDayTextColor);
|
||||
mMonthTitlePaint.setTextAlign(Align.CENTER);
|
||||
mMonthTitlePaint.setStyle(Style.FILL);
|
||||
|
||||
mSelectedCirclePaint = new Paint();
|
||||
mSelectedCirclePaint.setFakeBoldText(true);
|
||||
mSelectedCirclePaint.setAntiAlias(true);
|
||||
mSelectedCirclePaint.setColor(mTodayNumberColor);
|
||||
mSelectedCirclePaint.setTextAlign(Align.CENTER);
|
||||
mSelectedCirclePaint.setStyle(Style.FILL);
|
||||
mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
|
||||
|
||||
mMonthDayLabelPaint = new Paint();
|
||||
mMonthDayLabelPaint.setAntiAlias(true);
|
||||
mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
|
||||
mMonthDayLabelPaint.setColor(mMonthDayTextColor);
|
||||
mMonthTitlePaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.BOLD));
|
||||
mMonthDayLabelPaint.setStyle(Style.FILL);
|
||||
mMonthDayLabelPaint.setTextAlign(Align.CENTER);
|
||||
mMonthDayLabelPaint.setFakeBoldText(true);
|
||||
|
||||
mMonthNumPaint = new Paint();
|
||||
mMonthNumPaint.setAntiAlias(true);
|
||||
mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
|
||||
mMonthNumPaint.setStyle(Style.FILL);
|
||||
mMonthNumPaint.setTextAlign(Align.CENTER);
|
||||
mMonthNumPaint.setFakeBoldText(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
drawMonthTitle(canvas);
|
||||
drawMonthDayLabels(canvas);
|
||||
drawMonthNums(canvas);
|
||||
}
|
||||
|
||||
private int mDayOfWeekStart = 0;
|
||||
|
||||
/**
|
||||
* Sets all the parameters for displaying this week. The only required
|
||||
* parameter is the week number. Other parameters have a default value and
|
||||
* will only update if a new value is included, except for focus month,
|
||||
* which will always default to no focus month if no value is passed in.
|
||||
*/
|
||||
public void setMonthParams(int selectedDay, int year, int month, int weekStart) {
|
||||
if (month == -1 && year == -1) {
|
||||
throw new InvalidParameterException("You must specify month and year for this view");
|
||||
}
|
||||
|
||||
mSelectedDay = selectedDay;
|
||||
|
||||
// Allocate space for caching the day numbers and focus values
|
||||
mMonth = month;
|
||||
mYear = year;
|
||||
|
||||
// Figure out what day today is
|
||||
//final Time today = new Time(Time.getCurrentTimezone());
|
||||
//today.setToNow();
|
||||
final Calendar today = Calendar.getInstance(mController.getTimeZone(), mController.getLocale());
|
||||
mHasToday = false;
|
||||
mToday = -1;
|
||||
|
||||
mCalendar.set(Calendar.MONTH, mMonth);
|
||||
mCalendar.set(Calendar.YEAR, mYear);
|
||||
mCalendar.set(Calendar.DAY_OF_MONTH, 1);
|
||||
mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
|
||||
|
||||
if (weekStart != -1) {
|
||||
mWeekStart = weekStart;
|
||||
} else {
|
||||
mWeekStart = mCalendar.getFirstDayOfWeek();
|
||||
}
|
||||
|
||||
mNumCells = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH);
|
||||
for (int i = 0; i < mNumCells; i++) {
|
||||
final int day = i + 1;
|
||||
if (sameDay(day, today)) {
|
||||
mHasToday = true;
|
||||
mToday = day;
|
||||
}
|
||||
}
|
||||
mNumRows = calculateNumRows();
|
||||
|
||||
// Invalidate cached accessibility information.
|
||||
mTouchHelper.invalidateRoot();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void setSelectedDay(int day) {
|
||||
mSelectedDay = day;
|
||||
}
|
||||
|
||||
private int calculateNumRows() {
|
||||
int offset = findDayOffset();
|
||||
int dividend = (offset + mNumCells) / mNumDays;
|
||||
int remainder = (offset + mNumCells) % mNumDays;
|
||||
return (dividend + (remainder > 0 ? 1 : 0));
|
||||
}
|
||||
|
||||
private boolean sameDay(int day, Calendar today) {
|
||||
return mYear == today.get(Calendar.YEAR) &&
|
||||
mMonth == today.get(Calendar.MONTH) &&
|
||||
day == today.get(Calendar.DAY_OF_MONTH);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows + getMonthHeaderSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
mWidth = w;
|
||||
|
||||
// Invalidate cached accessibility information.
|
||||
mTouchHelper.invalidateRoot();
|
||||
}
|
||||
|
||||
public int getMonth() {
|
||||
return mMonth;
|
||||
}
|
||||
|
||||
public int getYear() {
|
||||
return mYear;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The height in pixels of a row of day labels
|
||||
*/
|
||||
public int getMonthHeight() {
|
||||
int scaleFactor = mController.getVersion() == DatePickerDialog.Version.VERSION_1 ? 2 : 3;
|
||||
return getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE * scaleFactor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The width in pixels of a day label
|
||||
*/
|
||||
public int getCellWidth() {
|
||||
return (mWidth - mEdgePadding * 2) / mNumDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The left / right padding used when calculating day number positions
|
||||
*/
|
||||
public int getEdgePadding() {
|
||||
return mEdgePadding;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper to the MonthHeaderSize to allow override it in children
|
||||
*/
|
||||
protected int getMonthHeaderSize() {
|
||||
return mController.getVersion() == DatePickerDialog.Version.VERSION_1
|
||||
? MONTH_HEADER_SIZE
|
||||
: MONTH_HEADER_SIZE_V2;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String getMonthAndYearString() {
|
||||
Locale locale = mController.getLocale();
|
||||
String pattern = "MMMM yyyy";
|
||||
|
||||
if (Build.VERSION.SDK_INT < 18) pattern = getContext().getResources().getString(R.string.mdtp_date_v1_monthyear);
|
||||
else pattern = DateFormat.getBestDateTimePattern(locale, pattern);
|
||||
|
||||
SimpleDateFormat formatter = new SimpleDateFormat(pattern, locale);
|
||||
formatter.setTimeZone(mController.getTimeZone());
|
||||
formatter.applyLocalizedPattern(pattern);
|
||||
mStringBuilder.setLength(0);
|
||||
return formatter.format(mCalendar.getTime());
|
||||
}
|
||||
|
||||
protected void drawMonthTitle(Canvas canvas) {
|
||||
int x = mWidth / 2;
|
||||
int y = mController.getVersion() == DatePickerDialog.Version.VERSION_1
|
||||
? (getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE) / 2
|
||||
: getMonthHeaderSize() / 2 - MONTH_DAY_LABEL_TEXT_SIZE;
|
||||
canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
|
||||
}
|
||||
|
||||
protected void drawMonthDayLabels(Canvas canvas) {
|
||||
int y = getMonthHeaderSize() - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
|
||||
int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2);
|
||||
|
||||
for (int i = 0; i < mNumDays; i++) {
|
||||
int x = (2 * i + 1) * dayWidthHalf + mEdgePadding;
|
||||
|
||||
int calendarDay = (i + mWeekStart) % mNumDays;
|
||||
mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
|
||||
String weekString = getWeekDayLabel(mDayLabelCalendar);
|
||||
canvas.drawText(weekString, x, y, mMonthDayLabelPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the week and month day numbers for this week. Override this method
|
||||
* if you need different placement.
|
||||
*
|
||||
* @param canvas The canvas to draw on
|
||||
*/
|
||||
protected void drawMonthNums(Canvas canvas) {
|
||||
int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
|
||||
+ getMonthHeaderSize();
|
||||
// TODO: look at the calculations used by the framework picker to properly align this with the buttons
|
||||
final int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2);
|
||||
int j = findDayOffset();
|
||||
for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
|
||||
final int x = (2 * j + 1) * dayWidthHalf + mEdgePadding;
|
||||
|
||||
int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH;
|
||||
|
||||
final int startX = x - dayWidthHalf;
|
||||
final int stopX = x + dayWidthHalf;
|
||||
final int startY = y - yRelativeToDay;
|
||||
final int stopY = startY + mRowHeight;
|
||||
|
||||
drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY);
|
||||
|
||||
j++;
|
||||
if (j == mNumDays) {
|
||||
j = 0;
|
||||
y += mRowHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should draw the month day. Implemented by sub-classes to allow customization.
|
||||
*
|
||||
* @param canvas The canvas to draw on
|
||||
* @param year The year of this month day
|
||||
* @param month The month of this month day
|
||||
* @param day The day number of this month day
|
||||
* @param x The default x position to draw the day number
|
||||
* @param y The default y position to draw the day number
|
||||
* @param startX The left boundary of the day number rect
|
||||
* @param stopX The right boundary of the day number rect
|
||||
* @param startY The top boundary of the day number rect
|
||||
* @param stopY The bottom boundary of the day number rect
|
||||
*/
|
||||
public abstract void drawMonthDay(Canvas canvas, int year, int month, int day,
|
||||
int x, int y, int startX, int stopX, int startY, int stopY);
|
||||
|
||||
protected int findDayOffset() {
|
||||
return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
|
||||
- mWeekStart;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the day that the given x position is in, accounting for week
|
||||
* number. Returns the day or -1 if the position wasn't in a day.
|
||||
*
|
||||
* @param x The x position of the touch event
|
||||
* @return The day number, or -1 if the position wasn't in a day
|
||||
*/
|
||||
public int getDayFromLocation(float x, float y) {
|
||||
final int day = getInternalDayFromLocation(x, y);
|
||||
if (day < 1 || day > mNumCells) {
|
||||
return -1;
|
||||
}
|
||||
return day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the day that the given x position is in, accounting for week
|
||||
* number.
|
||||
*
|
||||
* @param x The x position of the touch event
|
||||
* @return The day number
|
||||
*/
|
||||
protected int getInternalDayFromLocation(float x, float y) {
|
||||
int dayStart = mEdgePadding;
|
||||
if (x < dayStart || x > mWidth - mEdgePadding) {
|
||||
return -1;
|
||||
}
|
||||
// Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
|
||||
int row = (int) (y - getMonthHeaderSize()) / mRowHeight;
|
||||
int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mEdgePadding));
|
||||
|
||||
int day = column - findDayOffset() + 1;
|
||||
day += row * mNumDays;
|
||||
return day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks on a day. Handles callbacks to the
|
||||
* {@link OnDayClickListener} if one is set.
|
||||
* <p/>
|
||||
* If the day is out of the range set by minDate and/or maxDate, this is a no-op.
|
||||
*
|
||||
* @param day The day that was clicked
|
||||
*/
|
||||
private void onDayClick(int day) {
|
||||
// If the min / max date are set, only process the click if it's a valid selection.
|
||||
if (mController.isOutOfRange(mYear, mMonth, day)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (mOnDayClickListener != null) {
|
||||
mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day, mController.getTimeZone()));
|
||||
}
|
||||
|
||||
// This is a no-op if accessibility is turned off.
|
||||
mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param year as an int
|
||||
* @param month as an int
|
||||
* @param day as an int
|
||||
* @return true if the given date should be highlighted
|
||||
*/
|
||||
protected boolean isHighlighted(int year, int month, int day) {
|
||||
return mController.isHighlighted(year, month, day);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a 1 or 2 letter String for use as a weekday label
|
||||
*
|
||||
* @param day The day for which to generate a label
|
||||
* @return The weekday label
|
||||
*/
|
||||
private String getWeekDayLabel(Calendar day) {
|
||||
Locale locale = mController.getLocale();
|
||||
|
||||
// Localised short version of the string is not available on API < 18
|
||||
if (Build.VERSION.SDK_INT < 18) {
|
||||
String dayName = new SimpleDateFormat("E", locale).format(day.getTime());
|
||||
String dayLabel = dayName.toUpperCase(locale).substring(0, 1);
|
||||
|
||||
// Chinese labels should be fetched right to left
|
||||
if (locale.equals(Locale.CHINA) || locale.equals(Locale.CHINESE) || locale.equals(Locale.SIMPLIFIED_CHINESE) || locale.equals(Locale.TRADITIONAL_CHINESE)) {
|
||||
int len = dayName.length();
|
||||
dayLabel = dayName.substring(len - 1, len);
|
||||
}
|
||||
|
||||
// Most hebrew labels should select the second to last character
|
||||
if (locale.getLanguage().equals("he") || locale.getLanguage().equals("iw")) {
|
||||
if (mDayLabelCalendar.get(Calendar.DAY_OF_WEEK) != Calendar.SATURDAY) {
|
||||
int len = dayName.length();
|
||||
dayLabel = dayName.substring(len - 2, len - 1);
|
||||
} else {
|
||||
// I know this is duplication, but it makes the code easier to grok by
|
||||
// having all hebrew code in the same block
|
||||
dayLabel = dayName.toUpperCase(locale).substring(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Catalan labels should be two digits in lowercase
|
||||
if (locale.getLanguage().equals("ca"))
|
||||
dayLabel = dayName.toLowerCase().substring(0, 2);
|
||||
|
||||
// Correct single character label in Spanish is X
|
||||
if (locale.getLanguage().equals("es") && day.get(Calendar.DAY_OF_WEEK) == Calendar.WEDNESDAY)
|
||||
dayLabel = "X";
|
||||
|
||||
return dayLabel;
|
||||
}
|
||||
// Getting the short label is a one liner on API >= 18
|
||||
if (weekDayLabelFormatter == null) {
|
||||
weekDayLabelFormatter = new SimpleDateFormat("EEEEE", locale);
|
||||
}
|
||||
return weekDayLabelFormatter.format(day.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The date that has accessibility focus, or {@code null} if no date
|
||||
* has focus
|
||||
*/
|
||||
public CalendarDay getAccessibilityFocus() {
|
||||
final int day = mTouchHelper.getAccessibilityFocusedVirtualViewId();
|
||||
if (day >= 0) {
|
||||
return new CalendarDay(mYear, mMonth, day, mController.getTimeZone());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears accessibility focus within the view. No-op if the view does not
|
||||
* contain accessibility focus.
|
||||
*/
|
||||
public void clearAccessibilityFocus() {
|
||||
mTouchHelper.clearFocusedVirtualView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to restore accessibility focus to the specified date.
|
||||
*
|
||||
* @param day The date which should receive focus
|
||||
* @return {@code false} if the date is not valid for this month view, or
|
||||
* {@code true} if the date received focus
|
||||
*/
|
||||
public boolean restoreAccessibilityFocus(CalendarDay day) {
|
||||
if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
|
||||
return false;
|
||||
}
|
||||
mTouchHelper.setFocusedVirtualView(day.day);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a virtual view hierarchy for interfacing with an accessibility
|
||||
* service.
|
||||
*/
|
||||
protected class MonthViewTouchHelper extends ExploreByTouchHelper {
|
||||
private static final String DATE_FORMAT = "dd MMMM yyyy";
|
||||
|
||||
private final Rect mTempRect = new Rect();
|
||||
private final Calendar mTempCalendar = Calendar.getInstance(mController.getTimeZone());
|
||||
|
||||
MonthViewTouchHelper(View host) {
|
||||
super(host);
|
||||
}
|
||||
|
||||
void setFocusedVirtualView(int virtualViewId) {
|
||||
getAccessibilityNodeProvider(MonthView.this).performAction(
|
||||
virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
|
||||
}
|
||||
|
||||
void clearFocusedVirtualView() {
|
||||
final int focusedVirtualView = getAccessibilityFocusedVirtualViewId();
|
||||
if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
|
||||
getAccessibilityNodeProvider(MonthView.this).performAction(
|
||||
focusedVirtualView,
|
||||
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getVirtualViewAt(float x, float y) {
|
||||
final int day = getDayFromLocation(x, y);
|
||||
if (day >= 0) {
|
||||
return day;
|
||||
}
|
||||
return ExploreByTouchHelper.INVALID_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
|
||||
for (int day = 1; day <= mNumCells; day++) {
|
||||
virtualViewIds.add(day);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPopulateEventForVirtualView(int virtualViewId, @NonNull AccessibilityEvent event) {
|
||||
event.setContentDescription(getItemDescription(virtualViewId));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPopulateNodeForVirtualView(int virtualViewId,
|
||||
@NonNull AccessibilityNodeInfoCompat node) {
|
||||
getItemBounds(virtualViewId, mTempRect);
|
||||
|
||||
node.setContentDescription(getItemDescription(virtualViewId));
|
||||
node.setBoundsInParent(mTempRect);
|
||||
node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
|
||||
|
||||
// Flag non-selectable dates as disabled
|
||||
node.setEnabled(!mController.isOutOfRange(mYear, mMonth, virtualViewId));
|
||||
|
||||
if (virtualViewId == mSelectedDay) {
|
||||
node.setSelected(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
|
||||
Bundle arguments) {
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_CLICK:
|
||||
onDayClick(virtualViewId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the bounding rectangle of a given time object.
|
||||
*
|
||||
* @param day The day to calculate bounds for
|
||||
* @param rect The rectangle in which to store the bounds
|
||||
*/
|
||||
void getItemBounds(int day, Rect rect) {
|
||||
final int offsetX = mEdgePadding;
|
||||
final int offsetY = getMonthHeaderSize();
|
||||
final int cellHeight = mRowHeight;
|
||||
final int cellWidth = ((mWidth - (2 * mEdgePadding)) / mNumDays);
|
||||
final int index = ((day - 1) + findDayOffset());
|
||||
final int row = (index / mNumDays);
|
||||
final int column = (index % mNumDays);
|
||||
final int x = (offsetX + (column * cellWidth));
|
||||
final int y = (offsetY + (row * cellHeight));
|
||||
|
||||
rect.set(x, y, (x + cellWidth), (y + cellHeight));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a given time object. Since this
|
||||
* description will be spoken, the components are ordered by descending
|
||||
* specificity as DAY MONTH YEAR.
|
||||
*
|
||||
* @param day The day to generate a description for
|
||||
* @return A description of the time object
|
||||
*/
|
||||
CharSequence getItemDescription(int day) {
|
||||
mTempCalendar.set(mYear, mMonth, day);
|
||||
return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles callbacks when the user clicks on a time object.
|
||||
*/
|
||||
public interface OnDayClickListener {
|
||||
void onDayClick(MonthView view, CalendarDay day);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* A DayPickerView customized for {@link SimpleMonthAdapter}
|
||||
*/
|
||||
public class SimpleDayPickerView extends DayPickerView {
|
||||
|
||||
public SimpleDayPickerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public SimpleDayPickerView(Context context, DatePickerController controller) {
|
||||
super(context, controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MonthAdapter createMonthAdapter(DatePickerController controller) {
|
||||
return new SimpleMonthAdapter(controller);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* An adapter for a list of {@link SimpleMonthView} items.
|
||||
*/
|
||||
public class SimpleMonthAdapter extends MonthAdapter {
|
||||
|
||||
public SimpleMonthAdapter(DatePickerController controller) {
|
||||
super(controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MonthView createMonthView(Context context) {
|
||||
return new SimpleMonthView(context, null, mController);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Typeface;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class SimpleMonthView extends MonthView {
|
||||
|
||||
public SimpleMonthView(Context context, AttributeSet attr, DatePickerController controller) {
|
||||
super(context, attr, controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawMonthDay(Canvas canvas, int year, int month, int day,
|
||||
int x, int y, int startX, int stopX, int startY, int stopY) {
|
||||
if (mSelectedDay == day) {
|
||||
canvas.drawCircle(x, y - (MINI_DAY_NUMBER_TEXT_SIZE / 3), DAY_SELECTED_CIRCLE_SIZE,
|
||||
mSelectedCirclePaint);
|
||||
}
|
||||
|
||||
if (isHighlighted(year, month, day) && mSelectedDay != day) {
|
||||
canvas.drawCircle(x, y + MINI_DAY_NUMBER_TEXT_SIZE - DAY_HIGHLIGHT_CIRCLE_MARGIN,
|
||||
DAY_HIGHLIGHT_CIRCLE_SIZE, mSelectedCirclePaint);
|
||||
mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
|
||||
} else {
|
||||
mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.NORMAL));
|
||||
}
|
||||
|
||||
// gray out the day number if it's outside the range.
|
||||
if (mController.isOutOfRange(year, month, day)) {
|
||||
mMonthNumPaint.setColor(mDisabledDayTextColor);
|
||||
} else if (mSelectedDay == day) {
|
||||
mMonthNumPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
|
||||
mMonthNumPaint.setColor(mSelectedDayTextColor);
|
||||
} else if (mHasToday && mToday == day) {
|
||||
mMonthNumPaint.setColor(mTodayNumberColor);
|
||||
} else {
|
||||
mMonthNumPaint.setColor(isHighlighted(year, month, day) ? mHighlightedDayTextColor : mDayTextColor);
|
||||
}
|
||||
|
||||
canvas.drawText(String.format(mController.getLocale(), "%d", day), x, y, mMonthNumPaint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Paint.Style;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.R;
|
||||
|
||||
/**
|
||||
* A text view which, when pressed or activated, displays a colored circle around the text.
|
||||
*/
|
||||
public class TextViewWithCircularIndicator extends androidx.appcompat.widget.AppCompatTextView {
|
||||
|
||||
private static final int SELECTED_CIRCLE_ALPHA = 255;
|
||||
|
||||
Paint mCirclePaint = new Paint();
|
||||
|
||||
private int mCircleColor;
|
||||
private final String mItemIsSelectedText;
|
||||
|
||||
private boolean mDrawCircle;
|
||||
|
||||
public TextViewWithCircularIndicator(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mCircleColor = ContextCompat.getColor(context, R.color.mdtp_accent_color);
|
||||
mItemIsSelectedText = context.getResources().getString(R.string.mdtp_item_is_selected);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mCirclePaint.setFakeBoldText(true);
|
||||
mCirclePaint.setAntiAlias(true);
|
||||
mCirclePaint.setColor(mCircleColor);
|
||||
mCirclePaint.setTextAlign(Align.CENTER);
|
||||
mCirclePaint.setStyle(Style.FILL);
|
||||
mCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
|
||||
}
|
||||
|
||||
public void setAccentColor(int color, boolean darkMode) {
|
||||
mCircleColor = color;
|
||||
mCirclePaint.setColor(mCircleColor);
|
||||
setTextColor(createTextColor(color, darkMode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically set the color state list (see mdtp_date_picker_year_selector)
|
||||
* @param accentColor pressed state text color
|
||||
* @param darkMode current theme mode
|
||||
* @return ColorStateList with pressed state
|
||||
*/
|
||||
private ColorStateList createTextColor(int accentColor, boolean darkMode) {
|
||||
int[][] states = new int[][]{
|
||||
new int[]{android.R.attr.state_pressed}, // pressed
|
||||
new int[]{android.R.attr.state_selected}, // selected
|
||||
new int[]{}
|
||||
};
|
||||
int[] colors = new int[]{
|
||||
accentColor,
|
||||
Color.WHITE,
|
||||
darkMode ? Color.WHITE : Color.BLACK
|
||||
};
|
||||
return new ColorStateList(states, colors);
|
||||
}
|
||||
|
||||
public void drawIndicator(boolean drawCircle) {
|
||||
mDrawCircle = drawCircle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas canvas) {
|
||||
if (mDrawCircle) {
|
||||
final int width = getWidth();
|
||||
final int height = getHeight();
|
||||
int radius = Math.min(width, height) / 2;
|
||||
canvas.drawCircle(width / 2, height / 2, radius, mCirclePaint);
|
||||
}
|
||||
setSelected(mDrawCircle);
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
@SuppressLint("GetContentDescriptionOverride")
|
||||
@Override
|
||||
public CharSequence getContentDescription() {
|
||||
CharSequence itemText = getText();
|
||||
if (mDrawCircle) {
|
||||
return String.format(mItemIsSelectedText, itemText);
|
||||
} else {
|
||||
return itemText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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.date;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.StateListDrawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wdullaer.materialdatetimepicker.R;
|
||||
import com.wdullaer.materialdatetimepicker.date.DatePickerDialog.OnDateChangedListener;
|
||||
|
||||
/**
|
||||
* Displays a selectable list of years.
|
||||
*/
|
||||
public class YearPickerView extends ListView implements OnItemClickListener, OnDateChangedListener {
|
||||
private final DatePickerController mController;
|
||||
private YearAdapter mAdapter;
|
||||
private int mViewSize;
|
||||
private int mChildSize;
|
||||
private TextViewWithCircularIndicator mSelectedView;
|
||||
|
||||
public YearPickerView(Context context, DatePickerController controller) {
|
||||
super(context);
|
||||
mController = controller;
|
||||
mController.registerOnDateChangedListener(this);
|
||||
ViewGroup.LayoutParams frame = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
setLayoutParams(frame);
|
||||
Resources res = context.getResources();
|
||||
mViewSize = mController.getVersion() == DatePickerDialog.Version.VERSION_1
|
||||
? res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height)
|
||||
: res.getDimensionPixelOffset(R.dimen.mdtp_date_picker_view_animator_height_v2);
|
||||
mChildSize = res.getDimensionPixelOffset(R.dimen.mdtp_year_label_height);
|
||||
setVerticalFadingEdgeEnabled(true);
|
||||
setFadingEdgeLength(mChildSize / 3);
|
||||
init();
|
||||
setOnItemClickListener(this);
|
||||
setSelector(new StateListDrawable());
|
||||
setDividerHeight(0);
|
||||
onDateChanged();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mAdapter = new YearAdapter(mController.getMinYear(), mController.getMaxYear());
|
||||
setAdapter(mAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
mController.tryVibrate();
|
||||
TextViewWithCircularIndicator clickedView = (TextViewWithCircularIndicator) view;
|
||||
if (clickedView != null) {
|
||||
if (clickedView != mSelectedView) {
|
||||
if (mSelectedView != null) {
|
||||
mSelectedView.drawIndicator(false);
|
||||
mSelectedView.requestLayout();
|
||||
}
|
||||
clickedView.drawIndicator(true);
|
||||
clickedView.requestLayout();
|
||||
mSelectedView = clickedView;
|
||||
}
|
||||
mController.onYearSelected(getYearFromTextView(clickedView));
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static int getYearFromTextView(TextView view) {
|
||||
return Integer.valueOf(view.getText().toString());
|
||||
}
|
||||
|
||||
private final class YearAdapter extends BaseAdapter {
|
||||
private final int mMinYear;
|
||||
private final int mMaxYear;
|
||||
|
||||
YearAdapter(int minYear, int maxYear) {
|
||||
if (minYear > maxYear) {
|
||||
throw new IllegalArgumentException("minYear > maxYear");
|
||||
}
|
||||
mMinYear = minYear;
|
||||
mMaxYear = maxYear;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mMaxYear - mMinYear + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mMinYear + position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
TextViewWithCircularIndicator v;
|
||||
if (convertView != null) {
|
||||
v = (TextViewWithCircularIndicator) convertView;
|
||||
} else {
|
||||
v = (TextViewWithCircularIndicator) LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.mdtp_year_label_text_view, parent, false);
|
||||
v.setAccentColor(mController.getAccentColor(), mController.isThemeDark());
|
||||
}
|
||||
int year = mMinYear + position;
|
||||
boolean selected = mController.getSelectedDay().year == year;
|
||||
v.setText(String.format(mController.getLocale(),"%d", year));
|
||||
v.drawIndicator(selected);
|
||||
v.requestLayout();
|
||||
if (selected) {
|
||||
mSelectedView = v;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
public void postSetSelectionCentered(final int position) {
|
||||
postSetSelectionFromTop(position, mViewSize / 2 - mChildSize / 2);
|
||||
}
|
||||
|
||||
public void postSetSelectionFromTop(final int position, final int offset) {
|
||||
post(() -> {
|
||||
setSelectionFromTop(position, offset);
|
||||
requestLayout();
|
||||
});
|
||||
}
|
||||
|
||||
public int getFirstPositionOffset() {
|
||||
final View firstChild = getChildAt(0);
|
||||
if (firstChild == null) {
|
||||
return 0;
|
||||
}
|
||||
return firstChild.getTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDateChanged() {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
postSetSelectionCentered(mController.getSelectedDay().year - mController.getMinYear());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
|
||||
event.setFromIndex(0);
|
||||
event.setToIndex(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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