thellmund / Android-Week-View

Display highly customizable calendar views in your Android app
Apache License 2.0
188 stars 98 forks source link

Submitting list again to adapter causing java.lang.IllegalArgumentException: Layout: -2 < 0 #221

Closed kaspychala closed 3 years ago

kaspychala commented 3 years ago

Describe the bug After submitting list again in adapter application is crashing with error: java.lang.IllegalArgumentException: Layout: -2 < 0 in com.alamkanak.weekview.TextExtensionsKt.toTextLayout(TextExtensions.kt:29)

To Reproduce Steps to reproduce the behavior:

  1. Open fragment.
  2. Insert data into adapter.
  3. Go to another fragment.
  4. Go back to previous fragment.
  5. Insert data again into adapter.

Screenshots

image

Code to show content in WeekView

    private fun setUpWeekCalendar() {
        val adapter = object : WeekView.SimpleAdapter<Timetable>() {
            override fun onCreateEntity(item: Timetable): WeekViewEntity {
                return handleWeekCalendarOnCreateEntity(item)
            }
        }

        calendar_week_view.adapter = adapter
        viewModel.repository.timetable.value?.let {
            adapter.submitList(it)
        }

        calendar_week_view.setTimeFormatter {
            val date = Calendar.getInstance().apply {
                set(Calendar.HOUR_OF_DAY, it)
                set(Calendar.MINUTE, 0)
                set(Calendar.SECOND, 0)
                set(Calendar.MILLISECOND, 0)
            }

            viewModel.timeFormatter.format(date.time)
        }
    }

    private fun handleWeekCalendarOnCreateEntity(item: Timetable): WeekViewEntity {
        val backgroundColor = ContextCompat.getColor(requireContext(), R.color.colorPrimary)
        val textColor = Color.WHITE

        val style = WeekViewEntity.Style.Builder()
            .setTextColor(textColor)
            .setBackgroundColor(backgroundColor)
            .setBorderWidth(2)
            .setBorderColor(backgroundColor)
            .build()

        val title = SpannableStringBuilder(item.courseName).apply {
            val titleSpan = TypefaceSpan("sans-serif-medium")
            setSpan(titleSpan, 0, item.courseName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }

        val subtitle = SpannableStringBuilder(item.building?.name)

        return WeekViewEntity.Event.Builder(item)
            .setId(item.unitId.toLong())
            .setTitle(title)
            .setStartTime(item.startTime)
            .setEndTime(item.endTime)
            .setSubtitle(subtitle)
            .setAllDay(false)
            .setStyle(style)
            .build()
    }

function setUpWeekCalendar() is called after fetching data. Fetching data again by going back to fragment causing app to crash.

Maybe there is another way to refresh layout with new data but I'm aware of it?

Thanks for any help!

Additional context

thellmund commented 3 years ago

Please provide a sample repository that showcases the issue.

kaspychala commented 3 years ago

Can't, because it's company repo, but I can provide you classes from package and xml for layout.

class ScheduleFragment : BaseFragment<ScheduleViewModel, FragmentScheduleBinding>() {

    override val layoutRes = R.layout.fragment_schedule
    override val bindingVariable = BR.viewModel
    override val viewModelClass = ScheduleViewModel::class.java

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.getUserTimetable()
        viewModel.repository.isUserTimetableResultFetched.observe(viewLifecycleOwner, Observer {
            viewModel.getBuildingsDetails()
        })
        viewModel.repository.isBuildingResultFetched.observe(viewLifecycleOwner, Observer {
            viewModel.getLecturersDetails()
        })
        viewModel.repository.timetable.observe(viewLifecycleOwner, Observer { timetables->
            timetables.forEach { timetable->
                Timber.d("${timetable.courseName} - ${timetable.startTime}, ${timetable.endTime} - ${timetable.building?.name}, ${timetable.roomNumber}")
                timetable.lecturers.forEach { lecturer ->
                    Timber.d("${lecturer?.firstName} ${lecturer?.lastName} - ${lecturer?.profileUrl}")
                }
            }
            setUpView()
        })
    }

    override fun showApiProgressLoading() {
        //
    }

    private fun setUpView() {
        setUpSpinner()
        setUpWeekCalendar()
    }

    private fun setUpSpinner() {
        ArrayAdapter.createFromResource(
            requireContext(),
            R.array.schedule_calendar_modes,
            android.R.layout.simple_spinner_item
        ).also { adapter ->
            adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
            spinner_calendar_modes.adapter = adapter
        }

        spinner_calendar_modes.onItemSelectedListener =
            object : AdapterView.OnItemSelectedListener {

                override fun onItemSelected(
                    parent: AdapterView<*>,
                    view: View?,
                    pos: Int,
                    id: Long
                ) {
                    handleHeaderOnSpinnerItemSelected(pos)
                }

                override fun onNothingSelected(parent: AdapterView<*>) {}
            }

        spinner_calendar_modes.setSelection(ScheduleCalendarMode.DAILY.value)
    }

    private fun handleHeaderOnSpinnerItemSelected(pos: Int) {
        when (pos) {
            ScheduleCalendarMode.DAILY.value -> {
                calendar_week_view.maxDate = Calendar.getInstance().apply { time = viewModel.currentDate }
                calendar_week_view.minDate = Calendar.getInstance().apply { time = viewModel.currentDate }
                calendar_week_view.numberOfVisibleDays = 1
            }
            ScheduleCalendarMode.WEEKLY.value -> {
                val rangedDate = Calendar.getInstance().apply {
                    time = Date.from(
                        Instant.from(
                            viewModel.currentDate.toInstant().atZone(ZoneId.systemDefault())
                                .toLocalDate()
                                .with(
                                    TemporalAdjusters.previousOrSame( DayOfWeek.MONDAY )
                                )
                                .atStartOfDay(ZoneId.systemDefault())
                        )
                    )
                }
                calendar_week_view.minDate = rangedDate
                rangedDate.add(Calendar.DATE, 4)
                calendar_week_view.maxDate = rangedDate
                calendar_week_view.numberOfVisibleDays = 5
            }
            ScheduleCalendarMode.SET_WEEK.value -> {
                //
            }
        }
    }

    //TODO: Refreshing fragment on WeekView mode crashing app
    private fun setUpWeekCalendar() {
        val adapter = object : WeekView.SimpleAdapter<Timetable>() {
            override fun onCreateEntity(item: Timetable): WeekViewEntity {
                return handleWeekCalendarOnCreateEntity(item)
            }
        }

        calendar_week_view.adapter = adapter
        viewModel.repository.timetable.value?.let {
            adapter.submitList(it)
        }

        calendar_week_view.setTimeFormatter {
            val date = Calendar.getInstance().apply {
                set(Calendar.HOUR_OF_DAY, it)
                set(Calendar.MINUTE, 0)
                set(Calendar.SECOND, 0)
                set(Calendar.MILLISECOND, 0)
            }

            viewModel.timeFormatter.format(date.time)
        }
    }

    private fun handleWeekCalendarOnCreateEntity(item: Timetable): WeekViewEntity {
        val backgroundColor = ContextCompat.getColor(requireContext(), R.color.colorPrimary)
        val textColor = Color.WHITE

        val style = WeekViewEntity.Style.Builder()
            .setTextColor(textColor)
            .setBackgroundColor(backgroundColor)
            .setBorderWidth(2)
            .setBorderColor(backgroundColor)
            .build()

        val title = SpannableStringBuilder(item.courseName).apply {
            val titleSpan = TypefaceSpan("sans-serif-medium")
            setSpan(titleSpan, 0, item.courseName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }

        val subtitle = SpannableStringBuilder(item.building?.name)

        return WeekViewEntity.Event.Builder(item)
            .setId(item.unitId.toLong())
            .setTitle(title)
            .setStartTime(item.startTime)
            .setEndTime(item.endTime)
            .setSubtitle(subtitle)
            .setAllDay(false)
            .setStyle(style)
            .build()
    }
}
class ScheduleViewModel @Inject constructor(
    var repository: USOSTimetableRepository
) : BaseViewModel() {

    private val _text = MutableLiveData<String>().apply {
        value = "This is schedule fragment"
    }
    val text: LiveData<String> = _text

    private val is24HourFormat = DateFormat.is24HourFormat(App.applicationContext())
    val timeFormatter = if (is24HourFormat) {
        SimpleDateFormat("HH:mm", Locale.getDefault()).apply {
            timeZone = TimeZone.getTimeZone(ZoneId.systemDefault())
        }
    } else {
        SimpleDateFormat("hh:mm a", Locale.getDefault()).apply {
            timeZone = TimeZone.getTimeZone(ZoneId.systemDefault())
        }
    }

    var currentDate: Date = Date()

    fun getUserTimetable(){
        repository.getUserTimetable()
    }

    fun getBuildingsDetails() {
        repository.getBuildingsDetails()
    }

    fun getLecturersDetails() {
        repository.getLecturersDetails()
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="viewModel"
            type="pl.americansystems.smartuj.ui.mobileusos.schedule.ScheduleViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/cl_header"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            app:layout_constraintBottom_toTopOf="@id/cl_calendar_week_view_frame"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{viewModel.text}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toStartOf="@id/spinner_calendar_modes"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <Spinner
                android:id="@+id/spinner_calendar_modes"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@id/tv_title"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/cl_calendar_week_view_frame"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/cl_header">

            <com.alamkanak.weekview.WeekView
                android:id="@+id/calendar_week_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:adaptiveEventTextSize="true"
                app:allDayEventTextSize="13sp"
                app:columnGap="6dp"
                app:eventCornerRadius="4dp"
                app:eventMarginVertical="2dp"
                app:eventPaddingHorizontal="4dp"
                app:eventPaddingVertical="5dp"
                app:eventTextSize="13sp"
                app:headerBottomShadowRadius="1dp"
                app:headerPadding="8dp"
                app:headerTextColor="@color/colorPrimary"
                app:hourHeight="60dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:nowLineDotRadius="5dp"
                app:nowLineStrokeWidth="2dp"
                app:numberOfVisibleDays="7"
                app:overlappingEventGap="1dp"
                app:showCurrentTimeFirst="true"
                app:showHeaderBottomShadow="true"
                app:showNowLine="true"
                app:showNowLineDot="true"
                app:showTimeColumnSeparator="true"
                app:showWeekNumber="false"
                app:singleDayHorizontalPadding="8dp"
                app:timeColumnPadding="16dp"
                app:timeColumnSeparatorStrokeWidth="1dp"
                app:timeColumnTextColor="@color/colorPrimary"
                app:timeColumnTextSize="12sp"
                app:todayHeaderTextColor="@color/colorAccent"
                app:weekNumberBackgroundCornerRadius="8dp" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

For me it looks like it want to add same element in the same time section, but it can't fit another entry and it's crashing. Maybe it should skip drawing element with same id? Best solution will be to have method that will clear drawn entries before submitting new list, I think.

thellmund commented 3 years ago

Can't, because it's company repo

You don’t need to share the original repo. Create a new sample project (probably with sample data) that showcases the crash. Then, I can run it and see for myself.

kaspychala commented 3 years ago

https://github.com/kaspychala/WeekViewBugDescription

So I changed your sample app and played with WithFragmentActivity a little, now there is a handler that submits list again after some time. What is strange crash isn't happening now, but entries are disappearing. Even after submitting third list view isn't redrawing/adding new elements.

thellmund commented 3 years ago

The issue you’re seeing is actually an issue in the sample app, not in the library itself. The Calendar objects that are passed in to EventsDatabase#getEventsInRange(start, end) (here) are actually assigned different values within that method (see here). As a result, the second and third lists that are submitted contain no events. I’m updating the sample app to be less confusing.

As for the IllegalArgumentException mentioned at the top: I can’t fix the issue without being able to reproduce it. For that, I’d need you to provide me a sample app where this issue occurs.