prolificinteractive / material-calendarview

A Material design back port of Android's CalendarView
https://prolificinteractive.github.io/material-calendarview/
MIT License
5.92k stars 1.32k forks source link

How to change the text color of other months' dates? #933

Open chrismabotuwana opened 5 years ago

chrismabotuwana commented 5 years ago

I have set a different color for date text using

  <com.prolificinteractive.materialcalendarview.MaterialCalendarView
       ...
        app:mcv_dateTextAppearance="@style/CalendarDateTextAppearance"
        app:mcv_showOtherDates="other_months"/>

and the related style

    <style name="CalendarDateTextAppearance" parent="Home">
        <item name="android:textColor">@color/white</item>
        <item name="android:textSize">14sp</item>

    </style>

Therefore, all my date texts have the same color. I want my current month date texts to be white and other months' date texts to be grey. How can I achieve this?

chrismabotuwana commented 5 years ago

@quentin41500 Really appreciate your input here

hifeful commented 4 years ago

@chrismabotuwana Have you found a solution?

hifeful commented 4 years ago

Okay, I fixed that. I created OtherDaysDecorator for other days and DayDecorator for month days:

public class DayDecorator implements DayViewDecorator {
    private Context context;
    private MaterialCalendarView calendarView;

    public DayDecorator(Context context, MaterialCalendarView calendarView) {
        this.context = context;
        this.calendarView = calendarView;
    }

    @Override
    public boolean shouldDecorate(CalendarDay day) {
        Calendar cal1 = day.getCalendar();
        Calendar cal2 = calendarView.getCurrentDate().getCalendar();

        return cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
                cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
                cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH);
    }

    @Override
    public void decorate(DayViewFacade view) {
        view.addSpan(new TextAppearanceSpan(context, R.style.Day));
    }
public class OtherDaysDecorator implements DayViewDecorator {
    private Context context;
    private MaterialCalendarView calendarView;

    public OtherDaysDecorator(Context context, MaterialCalendarView calendarView) {
        this.context = context;
        this.calendarView = calendarView;
    }

    @Override
    public boolean shouldDecorate(CalendarDay day) {
        Calendar cal1 = day.getCalendar();
        Calendar cal2 = calendarView.getCurrentDate().getCalendar();

        return cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
                cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
                cal1.get(Calendar.MONTH) != cal2.get(Calendar.MONTH);
    }

    @Override
    public void decorate(DayViewFacade view) {
        view.addSpan(new TextAppearanceSpan(context, R.style.OtherDay));
    }
}

R.style.Day it's a style with colors for month calendar days.

<style name="Day">
        <item name="android:textColor">@color/calendar_day_text_color</item>
</style>

Other days:

<style name="OtherDay">
        <item name="android:textColor">@color/calendar_otherday_text_color</item>
</style>

And my xml files using in these styles:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:color="@color/colorBackground"
        android:state_checked="true"/>
    <item
        android:color="@color/colorOnSurface"
        android:state_pressed="true"/>
    <item
        android:color="@color/greyMaterial"
        android:state_enabled="false"/>
    <item android:color="@color/colorOnSurface"/>
</selector>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:color="@color/colorBackground"
        android:state_checked="true"/>
    <item
        android:color="@color/colorOnSurface"
        android:state_pressed="true"/>
    <item
        android:color="@color/greyMaterial"
        android:state_enabled="false"/>
    <item android:color="@android:color/holo_orange_dark"/>
</selector>

In my Fragment I create and add these decorators. Then, on every OnMonthClick I repaint my decorators.

calendarView = view.findViewById(R.id.records_calendar);
DayDecorator dayDecorator = new DayDecorator(getContext(), calendarView);
calendarView.addDecorator(dayDecorator);

calendarView.setOnMonthChangedListener(new OnMonthChangedListener() {
            @Override
            public void onMonthChanged(MaterialCalendarView widget, CalendarDay date) {
                Log.i(MainScreenActivity.TAG, "onMonthChanged: ");
                calendarView.removeDecorator(todayDecorator);
                calendarView.removeDecorator(daysDecorator);
                calendarView.removeDecorator(otherDaysDecorator);
                calendarView.invalidateDecorators();
                calendarView.addDecorator(daysDecorator);
                calendarView.addDecorator(otherDaysDecorator);
                calendarView.addDecorator(todayDecorator);
            }
        });

        otherDaysDecorator = new OtherDaysDecorator(getContext(), calendarView);
        calendarView.addDecorator(otherDaysDecorator);

        todayDecorator = new TodayDecorator(getContext());
        calendarView.addDecorator(todayDecorator);
honeykapoor7 commented 3 years ago

Calendar cal1 = day.getCalendar(); Calendar cal2 = calendarView.getCurrentDate().getCalendar();

These 2 lines give an error, unable to find the get calendar

Truerall commented 1 year ago

@chrismabotuwana, @honeykapoor7

Approach #2.

I was unhappy with using decorators to resolve this simple thing that should be essential, so I went a little bit dipper.

In DayView.java library has a problem, that actually leads to having Color.GRAY as not only default, but to use it as the only color.

if (!isInMonth && shouldBeVisible) {
      setTextColor(getTextColors().getColorForState(
          new int[] { -android.R.attr.state_enabled }, Color.GRAY));
    }

because of - sign before android.R.attr.state_enabled the color for attribute will never be found, and all users will have only 'Color.GRAY' as color for dates out of month.

If you remove -, there will be other problem - you will stuch with the color defined in android.R.attr.state_enabled.

The solution would be to use custom view states:

1) Import library's code to your project by any way that you prefere. 2) In values/attrs.xml declare custom attribute for DayView:

<declare-styleable name="DayViewStates">
    <attr name="state_out_of_month_enabled" format="boolean" />
  </declare-styleable>

3) In selector file that you use for defining of your text style (app:mcv_dateTextAppearance="@style/yourCustomTextStyleSelector"). Apply your new view state attribute using xmlns:app="http://schemas.android.com/apk/res-auto" namespace.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <!-- Don't forget about selector's short-circuit rule-->
    <item
        app:state_out_of_month_enabled="true"
        android:state_checked="false"
        android:state_pressed="false"
        android:color="@color/your_calendar_day_out_of_month_text_color" />

    <item android:color="@color/anyColorForStateChecked" android:state_checked="true" />
    <item android:color="@color/anyColorForStatePressed" android:state_pressed="true" />
    <item android:color="@color/your_calendar_regular_day_text_color"/>

</selector>

4) In DayView.java, you must override onCreateDrawableState and add your new state.

private static final int[] STATE_OUT_OF_MONTH_ENABLED = {R.attr.state_out_of_month_enabled};

@Override
protected int[] onCreateDrawableState(int extraSpace) {
    if (outOfMonthEnabled) {
    final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);

    mergeDrawableStates(drawableState, STATE_OUT_OF_MONTH_ENABLED);
    return drawableState;
    } else {
    return super.onCreateDrawableState(extraSpace);
    }
}

where outOfMonthEnabled is a global property that is based on

outOfMonthEnabled = !isInMonth && shouldBeVisible;

which was previously a codition to trigger the default color hardcoding

setTextColor(getTextColors().getColorForState(new int[] { -android.R.attr.state_enabled }, Color.GRAY));

To trigger state change you need to call refreshDrawableState() at the moment when your view goes to this state. In our case, it will be the end of setEnabled() method, right after outOfMonthEnabled is evaluated.

In the end, it does require you to fork the library, but as soon as its not really updated recently - why not? This way should be less memory CPU and consuming, in opose to constant invalidation of decorators.

@quentin41500 what do you think, is this a good idea to make a PR with this contribution?