From 016606c8f38df333703f04c255a3386a014483e0 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 15 Jul 2019 21:42:36 -0700 Subject: [PATCH 001/101] Adding Tablayout on Schedule, smooth scrolling to target --- .../shortstack/hackertracker/Utils/MyClock.kt | 11 ++- .../ui/schedule/ScheduleFragment.kt | 70 +++++++++++++++++++ .../ui/schedule/ScheduleViewModel.kt | 4 ++ .../ui/schedule/list/ScheduleAdapter.kt | 6 ++ .../src/main/res/layout/fragment_schedule.xml | 63 ++++++++++------- 5 files changed, 129 insertions(+), 25 deletions(-) diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/MyClock.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/MyClock.kt index 27d72ce0..9946a4d5 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/MyClock.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/MyClock.kt @@ -1,9 +1,18 @@ package com.shortstack.hackertracker.utils +import com.shortstack.hackertracker.BuildConfig +import java.text.SimpleDateFormat import java.util.* data class MyClock(val value: Int = 0) fun MyClock.now(): Date { + if(BuildConfig.DEBUG) { + return parse("2019-06-01T12:00:00.000-0000") + } return Date() -} \ No newline at end of file +} + +private fun parse(date: String): Date { + return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").parse(date) +} diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt index 0660734c..00b74be6 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt @@ -7,10 +7,13 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearSmoothScroller +import com.google.android.material.tabs.TabLayout import com.shortstack.hackertracker.R import com.shortstack.hackertracker.Status import com.shortstack.hackertracker.models.Day import com.shortstack.hackertracker.models.Time +import com.shortstack.hackertracker.models.local.Conference import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.ui.schedule.list.ScheduleAdapter import com.shortstack.hackertracker.utils.TickTimer @@ -19,6 +22,9 @@ import io.reactivex.disposables.Disposable import kotlinx.android.synthetic.main.fragment_schedule.* import kotlinx.android.synthetic.main.view_empty.view.* import org.koin.android.ext.android.inject +import java.text.SimpleDateFormat +import java.util.* +import kotlin.collections.ArrayList class ScheduleFragment : Fragment() { @@ -37,10 +43,17 @@ class ScheduleFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + shouldScroll = true list.adapter = adapter + val scheduleViewModel = ViewModelProviders.of(this).get(ScheduleViewModel::class.java) + scheduleViewModel.conference.observe(this, Observer { + addDayTabs(it) + }) + scheduleViewModel.schedule.observe(this, Observer { hideViews() @@ -69,6 +82,38 @@ class ScheduleFragment : Fragment() { } } }) + + tab_layout.addOnTabSelectedListener(object : TabLayout.BaseOnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab) { + val date = Date(tab.tag as Long) + scrollToDate(date) + } + + override fun onTabUnselected(tab: TabLayout.Tab) { + + } + + override fun onTabSelected(tab: TabLayout.Tab) { + val date = Date(tab.tag as Long) + scrollToDate(date) + } + }) + } + + private fun addDayTabs(conference: Conference) { + val days = getDaysBetweenDates(conference.startDate, conference.endDate) + conference.endDate + + tab_layout.removeAllTabs() + + val format = SimpleDateFormat("MMM d") + + for (day in days) { + val tab = tab_layout.newTab() + tab.text = format.format(day) + tab.tag = day.time + + tab_layout.addTab(tab) + } } private fun scrollToCurrentPosition(data: ArrayList) { @@ -94,6 +139,18 @@ class ScheduleFragment : Fragment() { return event } + private fun scrollToDate(date: Date) { + val index = adapter.getDatePosition(date) + if (index != -1) { + val scroller = object : LinearSmoothScroller(context) { + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + } + scroller.targetPosition = index + list.layoutManager?.startSmoothScroll(scroller) + } + } override fun onResume() { super.onResume() @@ -133,6 +190,19 @@ class ScheduleFragment : Fragment() { empty.visibility = View.VISIBLE } + fun getDaysBetweenDates(startdate: Date, enddate: Date): List { + val dates = ArrayList() + val calendar = GregorianCalendar() + calendar.time = startdate + + while (calendar.time.before(enddate)) { + val result = calendar.time + dates.add(result) + calendar.add(Calendar.DATE, 1) + } + return dates + } + companion object { fun newInstance() = ScheduleFragment() diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleViewModel.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleViewModel.kt index 8b1efb2a..db892217 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleViewModel.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import com.shortstack.hackertracker.Resource import com.shortstack.hackertracker.database.DatabaseManager +import com.shortstack.hackertracker.models.local.Conference import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.models.local.Type import org.koin.standalone.KoinComponent @@ -18,6 +19,9 @@ class ScheduleViewModel : ViewModel(), KoinComponent { val schedule: LiveData>> get() = contents + val conference: LiveData + get() = database.conference + private val contents = Transformations.switchMap(database.conference) { id -> val result = MediatorLiveData>>() diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt index 16e7c3fa..824f07bf 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt @@ -10,6 +10,8 @@ import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.ui.schedule.DayViewHolder import com.shortstack.hackertracker.ui.schedule.EventViewHolder import com.shortstack.hackertracker.ui.schedule.TimeViewHolder +import java.util.* +import kotlin.collections.ArrayList class ScheduleAdapter : RecyclerView.Adapter() { @@ -173,4 +175,8 @@ class ScheduleAdapter : RecyclerView.Adapter() { collection.clear() notifyDataSetChanged() } + + fun getDatePosition(date: Date): Int { + return collection.indexOfFirst { it is Day && it.time == date.time } + } } diff --git a/hackertracker/src/main/res/layout/fragment_schedule.xml b/hackertracker/src/main/res/layout/fragment_schedule.xml index 519a3154..4ec29643 100644 --- a/hackertracker/src/main/res/layout/fragment_schedule.xml +++ b/hackertracker/src/main/res/layout/fragment_schedule.xml @@ -1,34 +1,49 @@ - + android:orientation="vertical"> - - - - - + android:background="@color/colorPrimary" + app:tabMode="scrollable" /> + + + + + + + + + + + - + From f05fa68204fd3565d62194529281908d12e9e470 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 15 Jul 2019 21:53:45 -0700 Subject: [PATCH 002/101] Removing progress bar from Event View --- .../hackertracker/Utils/TimeUtil.kt | 10 +--- .../hackertracker/models/local/Event.kt | 2 +- .../hackertracker/ui/events/EventFragment.kt | 2 +- .../ui/schedule/TimeViewHolder.kt | 2 +- .../hackertracker/views/EventView.kt | 35 ------------ .../src/main/res/layout/row_event.xml | 54 ++++++++----------- .../hackertracker/utils/TimeUtilTest.kt | 8 +-- 7 files changed, 29 insertions(+), 84 deletions(-) diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/TimeUtil.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/TimeUtil.kt index 0501e0d2..00adf912 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/TimeUtil.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/TimeUtil.kt @@ -8,16 +8,8 @@ import java.util.* object TimeUtil { - private const val SOON_DAYS_AMOUNT = 5 - @SuppressLint("SimpleDateFormat") - fun getRelativeDateStamp(context: Context, date: Date): String { - if (date.isToday()) - return context.getString(R.string.today) - - if (date.isTomorrow()) - return context.getString(R.string.tomorrow) - + fun getDateStamp(date: Date): String { val format = SimpleDateFormat("MMMM d") return format.format(date) diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt index a61eb10b..7ce3a662 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt @@ -62,7 +62,7 @@ data class Event( fun getFullTimeStamp(context: Context): String { val (begin, end) = getTimeStamp(context) - val timestamp = TimeUtil.getRelativeDateStamp(context, start) + val timestamp = TimeUtil.getDateStamp(start) return String.format(context.getString(R.string.timestamp_full), timestamp, begin, end) } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt index f18967c8..2622e7cb 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/events/EventFragment.kt @@ -189,7 +189,7 @@ class EventFragment : Fragment() { fun getFullTimeStamp(context: Context, event: Event): String { val (begin, end) = getTimeStamp(context, event) - val timestamp = TimeUtil.getRelativeDateStamp(context, event.start) + val timestamp = TimeUtil.getDateStamp(event.start) return String.format(context.getString(R.string.timestamp_full), timestamp, begin, end) } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt index 203ff425..326a7eb3 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/TimeViewHolder.kt @@ -35,6 +35,6 @@ class DayViewHolder(val view: View) : RecyclerView.ViewHolder(view) { } fun render(day: Day) { - (view as TextView).text = TimeUtil.getRelativeDateStamp(view.context, day) + (view as TextView).text = TimeUtil.getDateStamp(day) } } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt index 8a1d4251..a4e1cbdd 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt @@ -91,7 +91,6 @@ class EventView : FrameLayout, KoinComponent { renderCategoryColour(event) updateBookmark(event) - setProgressBar(event) setOnClickListener { @@ -101,39 +100,6 @@ class EventView : FrameLayout, KoinComponent { star_bar.setOnClickListener { onBookmarkClick(event) } - - // TODO: Check that this works as intended instead of from within onAttachedToWindow(). - disposable?.dispose() - disposable = timer.observable.observeOn(AndroidSchedulers.mainThread()) - .subscribe { updateProgressBar(event) } - } - - private fun setProgressBar(event: Event) { - progress.progress = getProgress(event) - } - - private fun updateProgressBar(event: Event) { - val progress = getProgress(event) - - if (progress < this.progress.progress) { - setProgressBar(event) - return - } - - finishAnimation() - - val duration = PROGRESS_UPDATE_DURATION_PER_PERCENT * (progress - this.progress.progress) - - animation = ObjectAnimator.ofInt(this.progress, "progress", progress) - .also { - it.duration = duration.toLong() - it.interpolator = DecelerateInterpolator() - it.start() - } - } - - private fun getProgress(event: Event): Int { - return (event.progress * 100).toInt() } private fun renderCategoryColour(event: Event) { @@ -143,7 +109,6 @@ class EventView : FrameLayout, KoinComponent { val color = Color.parseColor(type.color) category.setBackgroundColor(color) - progress.progressDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { diff --git a/hackertracker/src/main/res/layout/row_event.xml b/hackertracker/src/main/res/layout/row_event.xml index ba7b1e1d..9246c3fb 100644 --- a/hackertracker/src/main/res/layout/row_event.xml +++ b/hackertracker/src/main/res/layout/row_event.xml @@ -15,20 +15,6 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - @@ -54,11 +42,9 @@ style="@style/TextAppearance.AppCompat.Body2" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" android:layout_marginTop="8dp" app:layout_constraintEnd_toStartOf="@+id/star_bar" - app:layout_constraintStart_toEndOf="@+id/category" + app:layout_constraintStart_toEndOf="@+id/guideline" app:layout_constraintTop_toTopOf="parent" tools:text="Compelled Decryption - State of the Art in Doctrinal Perversions" /> @@ -68,11 +54,11 @@ style="@style/TextAppearance.AppCompat.Body1" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" + android:layout_marginEnd="8dp" + android:layout_marginRight="8dp" android:textColor="@color/white_70" - app:layout_constraintEnd_toStartOf="@+id/star_bar" - app:layout_constraintStart_toEndOf="@+id/category" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/guideline" app:layout_constraintTop_toBottomOf="@+id/time" tools:text="Track 1" /> @@ -81,15 +67,13 @@ style="@style/TextAppearance.AppCompat.Body1" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginRight="8dp" android:textColor="@color/white_70" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toStartOf="@+id/star_bar" - app:layout_constraintStart_toEndOf="@+id/category" + app:layout_constraintStart_toEndOf="@+id/guideline" app:layout_constraintTop_toBottomOf="@+id/title" tools:text="Friday, 11:00 AM - 3:00 PM" /> @@ -98,16 +82,14 @@ style="@style/TextAppearance.AppCompat.Body1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" android:layout_marginTop="8dp" android:layout_marginBottom="16dp" android:background="@drawable/chip_background" - tools:text="Event" android:textColor="@color/white_70" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toEndOf="@+id/category" - app:layout_constraintTop_toBottomOf="@+id/location" /> + app:layout_constraintStart_toEndOf="@+id/guideline" + app:layout_constraintTop_toBottomOf="@+id/location" + tools:text="Event" /> + + \ No newline at end of file diff --git a/hackertracker/src/test/java/com/shortstack/hackertracker/utils/TimeUtilTest.kt b/hackertracker/src/test/java/com/shortstack/hackertracker/utils/TimeUtilTest.kt index 1c2d052d..01283cd1 100644 --- a/hackertracker/src/test/java/com/shortstack/hackertracker/utils/TimeUtilTest.kt +++ b/hackertracker/src/test/java/com/shortstack/hackertracker/utils/TimeUtilTest.kt @@ -7,13 +7,9 @@ import com.nhaarman.mockitokotlin2.whenever import com.shortstack.hackertracker.* import io.mockk.every import io.mockk.mockkStatic -import io.mockk.staticMockk -import io.mockk.use import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test -import java.text.SimpleDateFormat -import java.util.* class TimeUtilTest { @@ -53,7 +49,7 @@ class TimeUtilTest { val date = parse("2019-01-01T12:00:00.000-0000") - val result = TimeUtil.getRelativeDateStamp(context, date) + val result = TimeUtil.getDateStamp(date) assertEquals("Today", result) } @@ -63,7 +59,7 @@ class TimeUtilTest { setCurrentClock("2019-01-01T12:00:00.000-0000") val date = parse("2019-01-02T12:00:00.000-0000") - val result = TimeUtil.getRelativeDateStamp(context, date) + val result = TimeUtil.getDateStamp(date) assertEquals("Tomorrow", result) } From 2a49506a82e2d58e21ea7fa4cb2a0fc278ea4b18 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 15 Jul 2019 21:54:20 -0700 Subject: [PATCH 003/101] Removing dividers from Event View --- .../src/main/res/layout/row_event.xml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/hackertracker/src/main/res/layout/row_event.xml b/hackertracker/src/main/res/layout/row_event.xml index 9246c3fb..5ea2f82c 100644 --- a/hackertracker/src/main/res/layout/row_event.xml +++ b/hackertracker/src/main/res/layout/row_event.xml @@ -7,24 +7,6 @@ android:background="@color/background" android:orientation="horizontal"> - - - - - - Date: Mon, 15 Jul 2019 21:59:31 -0700 Subject: [PATCH 004/101] Changing Category from Chip to Dot --- .../hackertracker/views/EventView.kt | 2 +- .../src/main/res/drawable/chip_background.xml | 5 --- .../src/main/res/layout/row_event.xml | 35 ++++++++++++------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt index a4e1cbdd..912b93bf 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt @@ -114,7 +114,7 @@ class EventView : FrameLayout, KoinComponent { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val drawable = ContextCompat.getDrawable(context, R.drawable.chip_background)?.mutate() drawable?.setTint(color) - category_text.background = drawable + category_dot.background = drawable } } diff --git a/hackertracker/src/main/res/drawable/chip_background.xml b/hackertracker/src/main/res/drawable/chip_background.xml index 0bf96e22..5619167b 100644 --- a/hackertracker/src/main/res/drawable/chip_background.xml +++ b/hackertracker/src/main/res/drawable/chip_background.xml @@ -2,9 +2,4 @@ - \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/row_event.xml b/hackertracker/src/main/res/layout/row_event.xml index 5ea2f82c..39d7960b 100644 --- a/hackertracker/src/main/res/layout/row_event.xml +++ b/hackertracker/src/main/res/layout/row_event.xml @@ -29,47 +29,58 @@ app:layout_constraintStart_toEndOf="@+id/guideline" app:layout_constraintTop_toTopOf="parent" tools:text="Compelled Decryption - State of the Art in Doctrinal Perversions" /> - - + + app:layout_constraintTop_toBottomOf="@+id/title" + tools:text="Friday, 11:00 AM - 3:00 PM" /> + app:layout_constraintTop_toBottomOf="@+id/time" + tools:text="Track 1" /> + + From 2e94200b6756b4b0effb9c5cfd026c8dd777e03f Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 15 Jul 2019 22:46:41 -0700 Subject: [PATCH 005/101] Styling Day/Time View Holders --- .../hackertracker/Utils/TimeUtil.kt | 2 +- .../ui/schedule/list/ScheduleAdapter.kt | 43 +---------------- .../hackertracker/views/TimeView.kt | 47 ------------------- .../src/main/res/layout/row_event.xml | 4 +- .../src/main/res/layout/row_header.xml | 2 +- .../src/main/res/layout/row_header_time.xml | 25 +++------- hackertracker/src/main/res/values/dimens.xml | 1 + 7 files changed, 12 insertions(+), 112 deletions(-) diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/TimeUtil.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/TimeUtil.kt index 00adf912..bafa616d 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/TimeUtil.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/TimeUtil.kt @@ -25,7 +25,7 @@ object TimeUtil { return if (android.text.format.DateFormat.is24HourFormat(context)) { SimpleDateFormat("HH:mm").format(date) } else { - SimpleDateFormat("h:mm aa").format(date) + SimpleDateFormat("h:mm\naa").format(date) } } } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt index 824f07bf..f7ce4a82 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt @@ -74,48 +74,6 @@ class ScheduleAdapter : RecyclerView.Adapter() { return result } - fun notifyTimeChanged() { - if (collection.isEmpty()) - return - - val list = collection.toList() - - list.forEach { - if (it is Event && it.hasFinished) { - removeAndNotify(it) - } - } - - - if (list.size != this.collection.size) { - - for (i in this.collection.size - 1 downTo 1) { - val any = this.collection[i] - val any1 = this.collection[i - 1] - if ((any is Day && any1 is Day) - || (any is Time && any1 is Time) - || (any is Day && any1 is Time)) { - removeAndNotify(any1) - } - } - - // If no events and only headers remain. - if (collection.size == 2) { - val size = list.size - collection.clear() - notifyItemRangeRemoved(0, size) - } - } - } - - private fun removeAndNotify(item: Any) { - val index = collection.indexOf(item) - if (index != -1) { - collection.removeAt(index) - notifyItemRemoved(index) - } - } - fun setSchedule(list: List?): ArrayList { if (list == null) { val size = collection.size @@ -171,6 +129,7 @@ class ScheduleAdapter : RecyclerView.Adapter() { } fun isEmpty() = state == Status.SUCCESS && collection.isEmpty() + fun clearAndNotify() { collection.clear() notifyDataSetChanged() diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/TimeView.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/views/TimeView.kt index 9e3fbeb5..29d42f8a 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/TimeView.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/views/TimeView.kt @@ -2,49 +2,22 @@ package com.shortstack.hackertracker.views import android.content.Context import android.util.AttributeSet -import android.view.View import android.widget.LinearLayout import com.shortstack.hackertracker.R -import com.shortstack.hackertracker.getDateDifference -import com.shortstack.hackertracker.isToday -import com.shortstack.hackertracker.utils.MyClock -import com.shortstack.hackertracker.utils.TickTimer import com.shortstack.hackertracker.utils.TimeUtil -import com.shortstack.hackertracker.utils.now -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable import kotlinx.android.synthetic.main.row_header_time.view.* import org.koin.standalone.KoinComponent -import org.koin.standalone.inject import java.util.* -import java.util.concurrent.TimeUnit class TimeView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs), KoinComponent { private lateinit var date: Date - private val timer: TickTimer by inject() - - private var disposable: Disposable? = null - init { inflate(context, R.layout.row_header_time, this) orientation = LinearLayout.VERTICAL } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - disposable = timer.observable.observeOn(AndroidSchedulers.mainThread()) - .subscribe { render() } - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - disposable?.dispose() - disposable = null - } - fun setContent(content: Date) { date = content render() @@ -52,26 +25,6 @@ class TimeView(context: Context, attrs: AttributeSet) : LinearLayout(context, at fun render() { header.text = TimeUtil.getTimeStamp(context, date) - renderSubHeader() } - private fun renderSubHeader() { - val currentDate = MyClock().now() - - // Not today, or already started. - if (!date.isToday() || !date.after(currentDate)) { - subheader.visibility = View.GONE - return - } - - subheader.visibility = View.VISIBLE - - val inHours = currentDate.getDateDifference(date, TimeUnit.HOURS) - if (inHours >= 1) { - subheader.text = context.getString(R.string.msg_in_hours, inHours) - } else { - val inMinutes = currentDate.getDateDifference(date, TimeUnit.MINUTES) - subheader.text = context.getString(R.string.msg_in_mins, inMinutes) - } - } } diff --git a/hackertracker/src/main/res/layout/row_event.xml b/hackertracker/src/main/res/layout/row_event.xml index 39d7960b..0347de41 100644 --- a/hackertracker/src/main/res/layout/row_event.xml +++ b/hackertracker/src/main/res/layout/row_event.xml @@ -29,7 +29,7 @@ app:layout_constraintStart_toEndOf="@+id/guideline" app:layout_constraintTop_toTopOf="parent" tools:text="Compelled Decryption - State of the Art in Doctrinal Perversions" /> - + diff --git a/hackertracker/src/main/res/layout/row_header.xml b/hackertracker/src/main/res/layout/row_header.xml index c554bccc..c088f2c2 100644 --- a/hackertracker/src/main/res/layout/row_header.xml +++ b/hackertracker/src/main/res/layout/row_header.xml @@ -1,7 +1,7 @@ - + android:layout_width="@dimen/time_width" + android:layout_height="wrap_content"> + tools:text="10:00\nAM" /> - - - + diff --git a/hackertracker/src/main/res/values/dimens.xml b/hackertracker/src/main/res/values/dimens.xml index 88baf6b3..a56a9985 100644 --- a/hackertracker/src/main/res/values/dimens.xml +++ b/hackertracker/src/main/res/values/dimens.xml @@ -24,4 +24,5 @@ 5dp 1dp 8dp + 120dp From 22379ce6a0b524747fef97518f681234bb615070 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 16 Jul 2019 11:29:47 -0700 Subject: [PATCH 006/101] Adding sticky headers --- .../Utils/StickyHeaderInterface.kt | 36 ++++++ .../Utils/StickyHeaderItemDecoration.kt | 112 ++++++++++++++++++ .../ui/schedule/list/ScheduleAdapter.kt | 17 ++- .../hackertracker/views/EventView.kt | 3 + .../src/main/res/layout/row_event.xml | 11 ++ .../main/res/layout/row_time_container.xml | 1 + 6 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderInterface.kt create mode 100644 hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderItemDecoration.kt diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderInterface.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderInterface.kt new file mode 100644 index 00000000..2b0238a5 --- /dev/null +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderInterface.kt @@ -0,0 +1,36 @@ +package com.shortstack.hackertracker.utils + +import android.view.View + +interface StickyHeaderInterface { + + /** + * This method gets called by [StickHeaderItemDecoration] to fetch the position of the header item in the adapter + * that is used for (represents) item at specified position. + * @param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item. + * @return int. Position of the header item in the adapter. + */ + fun getHeaderPositionForItem(itemPosition: Int): Int + + /** + * This method gets called by [StickHeaderItemDecoration] to get layout resource id for the header item at specified adapter's position. + * @param headerPosition int. Position of the header item in the adapter. + * @return int. Layout resource id. + */ + fun getHeaderLayout(headerPosition: Int): Int + + /** + * This method gets called by [StickHeaderItemDecoration] to setup the header View. + * @param header View. Header to set the data on. + * @param headerPosition int. Position of the header item in the adapter. + */ + fun bindHeaderData(header: View, headerPosition: Int) + + /** + * This method gets called by [StickHeaderItemDecoration] to verify whether the item represents a header. + * @param itemPosition int. + * @return true, if item at the specified adapter's position represents a header. + */ + fun isHeader(itemPosition: Int): Boolean + +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderItemDecoration.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderItemDecoration.kt new file mode 100644 index 00000000..5166bd9f --- /dev/null +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderItemDecoration.kt @@ -0,0 +1,112 @@ +package com.shortstack.hackertracker.utils + +import android.graphics.Canvas +import android.view.LayoutInflater +import android.view.View +import android.view.View.MeasureSpec.EXACTLY +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + + +class StickyHeaderItemDecoration(private val mListener: StickyHeaderInterface) : RecyclerView.ItemDecoration() { + + private val mStickyHeaderHeight: Int = 0 + + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(c, parent, state) + val topChild = parent.getChildAt(0) ?: return + + val topChildPosition = parent.getChildAdapterPosition(topChild) + if (topChildPosition == RecyclerView.NO_POSITION) { + return + } + + val headerPos = mListener.getHeaderPositionForItem(topChildPosition) + val currentHeader = getHeaderViewForItem(headerPos, parent) + fixLayoutSize(parent, currentHeader) + val contactPoint = currentHeader.bottom + val childInContact = getChildInContact(parent, contactPoint, headerPos) + + if (childInContact != null && mListener.isHeader(parent.getChildAdapterPosition(childInContact))) { + moveHeader(c, currentHeader, childInContact) + return + } + + drawHeader(c, currentHeader) + } + + private fun getHeaderViewForItem(headerPosition: Int, parent: RecyclerView): View { + val layoutResId = mListener.getHeaderLayout(headerPosition) + val header = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false) + mListener.bindHeaderData(header, headerPosition) + return header + } + + private fun drawHeader(c: Canvas, header: View) { + c.save() + c.translate(0f, 0f) + header.draw(c) + c.restore() + } + + private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) { + c.save() + c.translate(0f, (nextHeader.top - currentHeader.height).toFloat()) + currentHeader.draw(c) + c.restore() + } + + private fun getChildInContact(parent: RecyclerView, contactPoint: Int, currentHeaderPos: Int): View? { + var childInContact: View? = null + for (i in 0 until parent.childCount) { + var heightTolerance = 0 + val child = parent.getChildAt(i) + + //measure height tolerance with child if child is another header + if (currentHeaderPos != i) { + val isChildHeader = mListener.isHeader(parent.getChildAdapterPosition(child)) + if (isChildHeader) { + heightTolerance = mStickyHeaderHeight - child.height + } + } + + //add heightTolerance if child top be in display area + val childBottomPosition: Int + if (child.top > 0) { + childBottomPosition = child.bottom + heightTolerance + } else { + childBottomPosition = child.bottom + } + + if (childBottomPosition > contactPoint) { + if (child.top <= contactPoint) { + // This child overlaps the contactPoint + childInContact = child + break + } + } + } + return childInContact + } + + + /** + * Properly measures and layouts the top sticky header. + * @param parent ViewGroup: RecyclerView in this case. + */ + private fun fixLayoutSize(parent: ViewGroup, view: View) { + + // Specs for parent (RecyclerView) + val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, EXACTLY) + val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) + + // Specs for children (headers) + val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.layoutParams.width) + val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height) + + view.measure(childWidthSpec, childHeightSpec) + + view.layout(0, 0, view.measuredWidth, view.measuredHeight) + } +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt index f7ce4a82..7fd45a08 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt @@ -1,5 +1,6 @@ package com.shortstack.hackertracker.ui.schedule.list +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -10,10 +11,24 @@ import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.ui.schedule.DayViewHolder import com.shortstack.hackertracker.ui.schedule.EventViewHolder import com.shortstack.hackertracker.ui.schedule.TimeViewHolder +import com.shortstack.hackertracker.utils.StickyHeaderInterface import java.util.* import kotlin.collections.ArrayList -class ScheduleAdapter : RecyclerView.Adapter() { +class ScheduleAdapter : RecyclerView.Adapter(){ +// override fun getHeaderPositionForItem(itemPosition: Int): Int { +// +// } +// +// override fun getHeaderLayout(headerPosition: Int): Int { +// +// } +// +// override fun bindHeaderData(header: View, headerPosition: Int) { +// +// } +// +// override fun isHeader(itemPosition: Int) = collection[itemPosition] is Time companion object { private const val EVENT = 0 diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt index 912b93bf..14aa10e3 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/views/EventView.kt @@ -93,6 +93,9 @@ class EventView : FrameLayout, KoinComponent { updateBookmark(event) + time_stamp.setContent(event.start) + + setOnClickListener { (context as? MainActivity)?.navigate(event) } diff --git a/hackertracker/src/main/res/layout/row_event.xml b/hackertracker/src/main/res/layout/row_event.xml index 0347de41..992bf39d 100644 --- a/hackertracker/src/main/res/layout/row_event.xml +++ b/hackertracker/src/main/res/layout/row_event.xml @@ -110,4 +110,15 @@ app:layout_constraintTop_toTopOf="parent" /> + + \ No newline at end of file diff --git a/hackertracker/src/main/res/layout/row_time_container.xml b/hackertracker/src/main/res/layout/row_time_container.xml index c3bbd200..d606b94b 100644 --- a/hackertracker/src/main/res/layout/row_time_container.xml +++ b/hackertracker/src/main/res/layout/row_time_container.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" tools:background="@color/background"> Date: Tue, 16 Jul 2019 15:18:36 -0700 Subject: [PATCH 007/101] Adding sticky headers --- hackertracker/build.gradle | 3 + .../Utils/StickyHeaderItemDecoration.kt | 112 --------------- .../hackertracker/models/local/Event.kt | 3 +- .../ui/schedule/ScheduleFragment.kt | 8 +- .../list/OverlapStickyItemDecoration.kt | 134 ++++++++++++++++++ .../ui/schedule/list/ScheduleAdapter.kt | 55 +++++-- .../src/main/res/layout/row_event.xml | 1 + .../src/main/res/layout/row_header_time.xml | 5 +- .../main/res/layout/row_time_container.xml | 1 - hackertracker/src/main/res/values/dimens.xml | 2 +- 10 files changed, 190 insertions(+), 134 deletions(-) delete mode 100644 hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderItemDecoration.kt create mode 100644 hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/OverlapStickyItemDecoration.kt diff --git a/hackertracker/build.gradle b/hackertracker/build.gradle index 01543319..a48bd13f 100644 --- a/hackertracker/build.gradle +++ b/hackertracker/build.gradle @@ -134,6 +134,9 @@ dependencies { testImplementation 'org.koin:koin-test:2.0.0-beta-4' + implementation 'com.timehop.stickyheadersrecyclerview:library:0.4.3@aar' + + } repositories { diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderItemDecoration.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderItemDecoration.kt deleted file mode 100644 index 5166bd9f..00000000 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/Utils/StickyHeaderItemDecoration.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.shortstack.hackertracker.utils - -import android.graphics.Canvas -import android.view.LayoutInflater -import android.view.View -import android.view.View.MeasureSpec.EXACTLY -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView - - -class StickyHeaderItemDecoration(private val mListener: StickyHeaderInterface) : RecyclerView.ItemDecoration() { - - private val mStickyHeaderHeight: Int = 0 - - - override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - super.onDrawOver(c, parent, state) - val topChild = parent.getChildAt(0) ?: return - - val topChildPosition = parent.getChildAdapterPosition(topChild) - if (topChildPosition == RecyclerView.NO_POSITION) { - return - } - - val headerPos = mListener.getHeaderPositionForItem(topChildPosition) - val currentHeader = getHeaderViewForItem(headerPos, parent) - fixLayoutSize(parent, currentHeader) - val contactPoint = currentHeader.bottom - val childInContact = getChildInContact(parent, contactPoint, headerPos) - - if (childInContact != null && mListener.isHeader(parent.getChildAdapterPosition(childInContact))) { - moveHeader(c, currentHeader, childInContact) - return - } - - drawHeader(c, currentHeader) - } - - private fun getHeaderViewForItem(headerPosition: Int, parent: RecyclerView): View { - val layoutResId = mListener.getHeaderLayout(headerPosition) - val header = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false) - mListener.bindHeaderData(header, headerPosition) - return header - } - - private fun drawHeader(c: Canvas, header: View) { - c.save() - c.translate(0f, 0f) - header.draw(c) - c.restore() - } - - private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) { - c.save() - c.translate(0f, (nextHeader.top - currentHeader.height).toFloat()) - currentHeader.draw(c) - c.restore() - } - - private fun getChildInContact(parent: RecyclerView, contactPoint: Int, currentHeaderPos: Int): View? { - var childInContact: View? = null - for (i in 0 until parent.childCount) { - var heightTolerance = 0 - val child = parent.getChildAt(i) - - //measure height tolerance with child if child is another header - if (currentHeaderPos != i) { - val isChildHeader = mListener.isHeader(parent.getChildAdapterPosition(child)) - if (isChildHeader) { - heightTolerance = mStickyHeaderHeight - child.height - } - } - - //add heightTolerance if child top be in display area - val childBottomPosition: Int - if (child.top > 0) { - childBottomPosition = child.bottom + heightTolerance - } else { - childBottomPosition = child.bottom - } - - if (childBottomPosition > contactPoint) { - if (child.top <= contactPoint) { - // This child overlaps the contactPoint - childInContact = child - break - } - } - } - return childInContact - } - - - /** - * Properly measures and layouts the top sticky header. - * @param parent ViewGroup: RecyclerView in this case. - */ - private fun fixLayoutSize(parent: ViewGroup, view: View) { - - // Specs for parent (RecyclerView) - val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, EXACTLY) - val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED) - - // Specs for children (headers) - val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.layoutParams.width) - val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height) - - view.measure(childWidthSpec, childHeightSpec) - - view.layout(0, 0, view.measuredWidth, view.measuredHeight) - } -} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt index 7ce3a662..b9e971d0 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/models/local/Event.kt @@ -22,7 +22,8 @@ data class Event( val speakers: List, val type: Type, val location: Location, - var isBookmarked: Boolean = false) : Parcelable { + var isBookmarked: Boolean = false, + var key: Long = -1) : Parcelable { val progress: Float get() { diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt index 00b74be6..8884effa 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt @@ -15,6 +15,7 @@ import com.shortstack.hackertracker.models.Day import com.shortstack.hackertracker.models.Time import com.shortstack.hackertracker.models.local.Conference import com.shortstack.hackertracker.models.local.Event +import com.shortstack.hackertracker.ui.schedule.list.OverlapStickyItemDecoration import com.shortstack.hackertracker.ui.schedule.list.ScheduleAdapter import com.shortstack.hackertracker.utils.TickTimer import io.reactivex.android.schedulers.AndroidSchedulers @@ -48,6 +49,9 @@ class ScheduleFragment : Fragment() { shouldScroll = true list.adapter = adapter + val decoration = OverlapStickyItemDecoration(adapter) + list.addItemDecoration(decoration) + val scheduleViewModel = ViewModelProviders.of(this).get(ScheduleViewModel::class.java) scheduleViewModel.conference.observe(this, Observer { @@ -122,8 +126,8 @@ class ScheduleFragment : Fragment() { if (shouldScroll) { shouldScroll = false - val index = getScrollIndex(data, first) - manager.scrollToPosition(index) +// val index = getScrollIndex(data, first) +// manager.scrollToPosition(index) } } diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/OverlapStickyItemDecoration.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/OverlapStickyItemDecoration.kt new file mode 100644 index 00000000..41ff8d0f --- /dev/null +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/OverlapStickyItemDecoration.kt @@ -0,0 +1,134 @@ +package com.shortstack.hackertracker.ui.schedule.list + +import android.graphics.Canvas +import android.graphics.Rect +import android.util.SparseArray +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.timehop.stickyheadersrecyclerview.HeaderPositionCalculator +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter +import com.timehop.stickyheadersrecyclerview.caching.HeaderProvider +import com.timehop.stickyheadersrecyclerview.caching.HeaderViewCache +import com.timehop.stickyheadersrecyclerview.calculation.DimensionCalculator +import com.timehop.stickyheadersrecyclerview.rendering.HeaderRenderer +import com.timehop.stickyheadersrecyclerview.util.LinearLayoutOrientationProvider +import com.timehop.stickyheadersrecyclerview.util.OrientationProvider + +class OverlapStickyItemDecoration(private val mAdapter: StickyRecyclerHeadersAdapter) : RecyclerView.ItemDecoration() { + + private val mHeaderRects = SparseArray() + private var mOrientationProvider: OrientationProvider = LinearLayoutOrientationProvider() + private var mHeaderProvider: HeaderProvider = HeaderViewCache(mAdapter, mOrientationProvider) + + private var mRenderer: HeaderRenderer = HeaderRenderer(mOrientationProvider) + private var mDimensionCalculator: DimensionCalculator = DimensionCalculator() + + private val mHeaderPositionCalculator: HeaderPositionCalculator = HeaderPositionCalculator(mAdapter, mHeaderProvider, mOrientationProvider, + mDimensionCalculator) + + /** + * The following field is used as a buffer for internal calculations. Its sole purpose is to avoid + * allocating new Rect every time we need one. + */ + private val mTempRect = Rect() + + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + super.getItemOffsets(outRect, view, parent, state) + val itemPosition = parent.getChildAdapterPosition(view) + if (itemPosition == RecyclerView.NO_POSITION) { + return + } + if (mHeaderPositionCalculator.hasNewHeader(itemPosition, mOrientationProvider.isReverseLayout(parent))) { + val header = getHeaderView(parent, itemPosition) + setItemOffsetsForHeader(outRect, header, mOrientationProvider.getOrientation(parent)) + } + } + + /** + * Sets the offsets for the first item in a section to make room for the header view + * + * @param itemOffsets rectangle to define offsets for the item + * @param header view used to calculate offset for the item + * @param orientation used to calculate offset for the item + */ + private fun setItemOffsetsForHeader(itemOffsets: Rect, header: View, orientation: Int) { + mDimensionCalculator.initMargins(mTempRect, header) + + if (orientation == LinearLayoutManager.VERTICAL) { + itemOffsets.top = header.height + mTempRect.top + mTempRect.bottom + itemOffsets.bottom = 0 + } else { + itemOffsets.left = header.width + mTempRect.left + mTempRect.right + } + } + + override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(canvas, parent, state) + + val childCount = parent.childCount + if (childCount <= 0 || mAdapter.itemCount <= 0) { + return + } + + for (i in 0 until childCount) { + val itemView = parent.getChildAt(i) + val position = parent.getChildAdapterPosition(itemView) + if (position == RecyclerView.NO_POSITION) { + continue + } + + val hasStickyHeader = mHeaderPositionCalculator.hasStickyHeader(itemView, mOrientationProvider.getOrientation(parent), position) + if (hasStickyHeader || mHeaderPositionCalculator.hasNewHeader(position, mOrientationProvider.isReverseLayout(parent))) { + val header = mHeaderProvider.getHeader(parent, position) + //re-use existing Rect, if any. + var headerOffset: Rect? = mHeaderRects.get(position) + if (headerOffset == null) { + headerOffset = Rect() + mHeaderRects.put(position, headerOffset) + } + mHeaderPositionCalculator.initHeaderBounds(headerOffset, parent, header, itemView, hasStickyHeader) + mRenderer.drawHeader(parent, canvas, header, headerOffset) + } + } + } + + /** + * Gets the position of the header under the specified (x, y) coordinates. + * + * @param x x-coordinate + * @param y y-coordinate + * @return position of header, or -1 if not found + */ + fun findHeaderPositionUnder(x: Int, y: Int): Int { + for (i in 0 until mHeaderRects.size()) { + val rect = mHeaderRects.get(mHeaderRects.keyAt(i)) + if (rect.contains(x, y)) { + return mHeaderRects.keyAt(i) + } + } + return -1 + } + + /** + * Gets the header view for the associated position. If it doesn't exist yet, it will be + * created, measured, and laid out. + * + * @param parent + * @param position + * @return Header view + */ + fun getHeaderView(parent: RecyclerView, position: Int): View { + return mHeaderProvider.getHeader(parent, position) + } + + /** + * Invalidates cached headers. This does not invalidate the recyclerview, you should do that manually after + * calling this method. + */ + fun invalidateHeaders() { + mHeaderProvider.invalidate() + } + +} \ No newline at end of file diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt index 7fd45a08..1b0a6eee 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/list/ScheduleAdapter.kt @@ -12,23 +12,45 @@ import com.shortstack.hackertracker.ui.schedule.DayViewHolder import com.shortstack.hackertracker.ui.schedule.EventViewHolder import com.shortstack.hackertracker.ui.schedule.TimeViewHolder import com.shortstack.hackertracker.utils.StickyHeaderInterface +import com.shortstack.hackertracker.views.TimeView +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter import java.util.* import kotlin.collections.ArrayList -class ScheduleAdapter : RecyclerView.Adapter(){ -// override fun getHeaderPositionForItem(itemPosition: Int): Int { -// -// } -// -// override fun getHeaderLayout(headerPosition: Int): Int { -// -// } -// -// override fun bindHeaderData(header: View, headerPosition: Int) { -// -// } -// -// override fun isHeader(itemPosition: Int) = collection[itemPosition] is Time +class ScheduleAdapter : RecyclerView.Adapter(), StickyRecyclerHeadersAdapter{ + + init { + setHasStableIds(true) + } + + override fun getHeaderId(position: Int): Long { + return when(val obj = collection[position]) { + is Day -> -1 + is Time -> obj.time + is Event -> obj.key + else -> throw java.lang.IllegalStateException("Unhandled object type ${obj.javaClass}") + } + } + + override fun getItemId(position: Int): Long { + when(val obj = collection[position]) { + is Time -> return obj.time + is Event -> return obj.key + } + return super.getItemId(position) + } + + override fun onCreateHeaderViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { + return TimeViewHolder.inflate(parent) + } + + override fun onBindHeaderViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + if(viewHolder is TimeViewHolder && collection[position] is Event) { + val event = collection[position] as Event + viewHolder.render(Time(Date(event.key))) + } + } + companion object { private const val EVENT = 0 @@ -77,10 +99,13 @@ class ScheduleAdapter : RecyclerView.Adapter(){ result.add(Day(it.key)) it.value.groupBy { it.start }.toSortedMap().forEach { - result.add(Time(it.key)) +// result.add(Time(it.key)) if (it.value.isNotEmpty()) { val group = it.value.sortedWith(compareBy({ it.type.name }, { it.location.name })) + + group.forEach { event -> event.key = it.key.time } + result.addAll(group) } } diff --git a/hackertracker/src/main/res/layout/row_event.xml b/hackertracker/src/main/res/layout/row_event.xml index 992bf39d..a4cb364e 100644 --- a/hackertracker/src/main/res/layout/row_event.xml +++ b/hackertracker/src/main/res/layout/row_event.xml @@ -116,6 +116,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" + android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/guideline" app:layout_constraintStart_toEndOf="@+id/category" diff --git a/hackertracker/src/main/res/layout/row_header_time.xml b/hackertracker/src/main/res/layout/row_header_time.xml index cb66f696..d0937024 100644 --- a/hackertracker/src/main/res/layout/row_header_time.xml +++ b/hackertracker/src/main/res/layout/row_header_time.xml @@ -10,7 +10,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - tools:background="@color/background" - tools:text="10:00\nAM" /> + android:padding="16dp" + android:text="10:00\nAM" + tools:background="@color/background" /> diff --git a/hackertracker/src/main/res/layout/row_time_container.xml b/hackertracker/src/main/res/layout/row_time_container.xml index d606b94b..c3bbd200 100644 --- a/hackertracker/src/main/res/layout/row_time_container.xml +++ b/hackertracker/src/main/res/layout/row_time_container.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" tools:background="@color/background"> 5dp 1dp 8dp - 120dp + 100dp From f38666e7b2823776535186b240ef85e869f1cdc3 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 17 Jul 2019 22:28:27 -0700 Subject: [PATCH 008/101] DaySelectorView --- .../ui/schedule/ScheduleFragment.kt | 47 +++-- .../ui/schedule/list/ScheduleAdapter.kt | 53 ++++-- .../hackertracker/views/DaySelectorView.kt | 174 ++++++++++++++++++ .../main/res/drawable/day_selector_bubble.xml | 6 + .../src/main/res/layout/fragment_schedule.xml | 7 + .../src/main/res/layout/row_event.xml | 6 +- .../src/main/res/layout/row_header.xml | 2 +- .../src/main/res/layout/row_header_time.xml | 2 +- .../src/main/res/layout/view_day.xml | 22 +++ .../src/main/res/layout/view_day_selector.xml | 51 +++++ hackertracker/src/main/res/values/colors.xml | 2 + hackertracker/src/main/res/values/styles.xml | 15 ++ 12 files changed, 350 insertions(+), 37 deletions(-) create mode 100644 hackertracker/src/main/java/com/shortstack/hackertracker/views/DaySelectorView.kt create mode 100644 hackertracker/src/main/res/drawable/day_selector_bubble.xml create mode 100644 hackertracker/src/main/res/layout/view_day.xml create mode 100644 hackertracker/src/main/res/layout/view_day_selector.xml diff --git a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt index 00b74be6..877856b8 100644 --- a/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt +++ b/hackertracker/src/main/java/com/shortstack/hackertracker/ui/schedule/ScheduleFragment.kt @@ -7,7 +7,9 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.tabs.TabLayout import com.shortstack.hackertracker.R import com.shortstack.hackertracker.Status @@ -16,7 +18,9 @@ import com.shortstack.hackertracker.models.Time import com.shortstack.hackertracker.models.local.Conference import com.shortstack.hackertracker.models.local.Event import com.shortstack.hackertracker.ui.schedule.list.ScheduleAdapter +import com.shortstack.hackertracker.utils.StickyHeaderItemDecoration import com.shortstack.hackertracker.utils.TickTimer +import com.shortstack.hackertracker.views.DaySelectorView import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import kotlinx.android.synthetic.main.fragment_schedule.* @@ -48,6 +52,28 @@ class ScheduleFragment : Fragment() { shouldScroll = true list.adapter = adapter +// val decoration = StickyHeaderItemDecoration(adapter) +// list.addItemDecoration(decoration) + + list.addOnScrollListener(object: RecyclerView.OnScrollListener(){ + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val manager = list.layoutManager as? LinearLayoutManager + if(manager != null ) { + val first = manager.findFirstVisibleItemPosition() + val last = manager.findLastVisibleItemPosition() + + day_selector.onScroll(adapter.getDateOfPosition(first), adapter.getDateOfPosition(last)) + } + } + }) + + day_selector.addOnDaySelectedListener(object: DaySelectorView.OnDaySelectedListener{ + override fun onDaySelected(day: Date) { + scrollToDate(day) + } + }) + val scheduleViewModel = ViewModelProviders.of(this).get(ScheduleViewModel::class.java) scheduleViewModel.conference.observe(this, Observer { @@ -82,22 +108,6 @@ class ScheduleFragment : Fragment() { } } }) - - tab_layout.addOnTabSelectedListener(object : TabLayout.BaseOnTabSelectedListener { - override fun onTabReselected(tab: TabLayout.Tab) { - val date = Date(tab.tag as Long) - scrollToDate(date) - } - - override fun onTabUnselected(tab: TabLayout.Tab) { - - } - - override fun onTabSelected(tab: TabLayout.Tab) { - val date = Date(tab.tag as Long) - scrollToDate(date) - } - }) } private fun addDayTabs(conference: Conference) { @@ -107,6 +117,8 @@ class ScheduleFragment : Fragment() { val format = SimpleDateFormat("MMM d") + day_selector.setDays(days) + for (day in days) { val tab = tab_layout.newTab() tab.text = format.format(day) @@ -129,7 +141,8 @@ class ScheduleFragment : Fragment() { private fun getScrollIndex(data: ArrayList, first: Event): Int { val event = data.indexOf(first) - val index = data.indexOf(data.subList(0, event).filterIsInstance