[UI] Add pager fragment templates. Move all templates to 'template' module. Fix swipe to refresh with pager fragments.

This commit is contained in:
Kuba Szczodrzyński 2020-03-30 12:50:21 +02:00
parent 043f8210ba
commit 8c869d082b
18 changed files with 223 additions and 79 deletions

View File

@ -73,6 +73,7 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesListFragment
import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment
import pl.szczodrzynski.edziennik.ui.modules.template.TemplateFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
@ -129,6 +130,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
const val TARGET_MESSAGES_DETAILS = 503
const val TARGET_MESSAGES_COMPOSE = 504
const val TARGET_WEB_PUSH = 140
const val TARGET_TEMPLATE = 1000
const val HOME_ID = DRAWER_ITEM_HOME
@ -227,6 +229,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class)
list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class)
list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class)
if (App.devMode) {
list += NavTarget(TARGET_TEMPLATE, R.string.menu_template, TemplateFragment::class)
.withIcon(CommunityMaterial.Icon2.cmd_test_tube_empty)
.isInDrawer(true)
.isBelowSeparator(true)
.isStatic(true)
}
list
}

View File

@ -1,28 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-22.
*/
package pl.szczodrzynski.edziennik.ui.modules.base
import androidx.fragment.app.Fragment
abstract class PagerFragment : Fragment() {
private var isPageCreated = false
/**
* Called when the page is first shown, or if previous
* [onPageCreated] returned false
*
* @return true if the view is set up
* @return false if the setup failed. The method may be then called
* again, when page becomes visible.
*/
abstract fun onPageCreated(): Boolean
override fun onResume() {
if (!isPageCreated) {
isPageCreated = onPageCreated()
}
super.onResume()
}
}

View File

@ -8,7 +8,8 @@ import androidx.fragment.app.Fragment
abstract class LazyFragment : Fragment() {
private var isPageCreated = false
var swipeRefreshLayoutCallback: ((isEnabled: Boolean) -> Unit)? = null
internal var position = -1
internal var swipeRefreshLayoutCallback: ((position: Int, isEnabled: Boolean) -> Unit)? = null
/**
* Called when the page is first shown, or if previous
@ -20,6 +21,10 @@ abstract class LazyFragment : Fragment() {
*/
abstract fun onPageCreated(): Boolean
fun enableSwipeToRefresh() = swipeRefreshLayoutCallback?.invoke(position, true)
fun disableSwipeToRefresh() = swipeRefreshLayoutCallback?.invoke(position, false)
fun setSwipeToRefresh(enabled: Boolean) = swipeRefreshLayoutCallback?.invoke(position, enabled)
internal fun createPage() {
if (!isPageCreated && isAdded) {
isPageCreated = onPageCreated()

View File

@ -4,10 +4,25 @@
package pl.szczodrzynski.edziennik.ui.modules.base.lazypager
import android.util.SparseBooleanArray
import androidx.core.util.set
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
abstract class LazyPagerAdapter(fragmentManager: FragmentManager) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
var swipeRefreshLayoutCallback: ((isEnabled: Boolean) -> Unit)? = null
abstract override fun getItem(position: Int): LazyFragment
abstract class LazyPagerAdapter(fragmentManager: FragmentManager, val swipeRefreshLayout: SwipeRefreshLayout? = null) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
internal val enabledList = SparseBooleanArray()
private val refreshLayoutCallback: (position: Int, isEnabled: Boolean) -> Unit = { position, isEnabled ->
swipeRefreshLayout?.isEnabled = isEnabled
if (position > -1)
enabledList[position] = isEnabled
}
final override fun getItem(position: Int): LazyFragment {
return getPage(position).also {
it.position = position
it.swipeRefreshLayoutCallback = refreshLayoutCallback
}
}
abstract fun getPage(position: Int): LazyFragment
abstract override fun getPageTitle(position: Int): CharSequence
}

View File

@ -11,10 +11,15 @@ import androidx.viewpager.widget.ViewPager
class LazyViewPager @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
var pageSelection = -1
init {
addOnPageChangeListener(object : OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
(adapter as? LazyPagerAdapter)?.let {
it.swipeRefreshLayout?.isEnabled = state == SCROLL_STATE_IDLE && it.enabledList[pageSelection, true]
}
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
@ -22,6 +27,7 @@ class LazyViewPager @JvmOverloads constructor(
}
override fun onPageSelected(position: Int) {
pageSelection = position
if (adapter is LazyPagerAdapter) {
val fragment = adapter?.instantiateItem(this@LazyViewPager, position)
val lazyFragment = fragment as? LazyFragment

View File

@ -67,10 +67,7 @@ class HomeworkFragment : Fragment() {
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
}))
b.viewPager.adapter = MessagesFragment.Adapter(childFragmentManager).also { adapter ->
adapter.swipeRefreshLayoutCallback = { isEnabled ->
b.refreshLayout.isEnabled = isEnabled
}
b.viewPager.adapter = MessagesFragment.Adapter(childFragmentManager, b.refreshLayout).also { adapter ->
adapter.addFragment(HomeworkListFragment().also { fragment ->
fragment.arguments = Bundle().also { args ->
@ -86,11 +83,8 @@ class HomeworkFragment : Fragment() {
}
b.viewPager.currentItem = pageSelection
b.viewPager.clearOnPageChangeListeners()
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE
}
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
pageSelection = position

View File

@ -45,10 +45,10 @@ class HomeworkListFragment : LazyFragment() {
b.homeworkView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (recyclerView.canScrollVertically(-1)) {
swipeRefreshLayoutCallback?.invoke(false)
setSwipeToRefresh(false)
}
if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
swipeRefreshLayoutCallback?.invoke(true)
setSwipeToRefresh(true)
}
}
})

View File

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewpager.widget.ViewPager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.App
@ -54,11 +55,7 @@ class MessagesFragment : Fragment() {
return
}
b.viewPager.adapter = Adapter(childFragmentManager).also { adapter ->
adapter.swipeRefreshLayoutCallback = { isEnabled ->
b.refreshLayout.isEnabled = isEnabled
}
b.viewPager.adapter = Adapter(childFragmentManager, b.refreshLayout).also { adapter ->
adapter.addFragment(MessagesListFragment().also { fragment ->
fragment.arguments = Bundle().also { args ->
@ -75,13 +72,8 @@ class MessagesFragment : Fragment() {
}
b.viewPager.currentItem = pageSelection
b.viewPager.clearOnPageChangeListeners()
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
if (b.refreshLayout != null) {
b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE
}
}
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
pageSelection = position
@ -130,11 +122,11 @@ class MessagesFragment : Fragment() {
}*/
}
internal class Adapter(manager: FragmentManager) : LazyPagerAdapter(manager) {
internal class Adapter(manager: FragmentManager, swipeRefreshLayout: SwipeRefreshLayout) : LazyPagerAdapter(manager, swipeRefreshLayout) {
private val mFragmentList = mutableListOf<LazyFragment>()
private val mFragmentTitleList = mutableListOf<String>()
override fun getItem(position: Int): LazyFragment {
override fun getPage(position: Int): LazyFragment {
return mFragmentList[position]
}
@ -143,12 +135,11 @@ class MessagesFragment : Fragment() {
}
fun addFragment(fragment: LazyFragment, title: String) {
fragment.swipeRefreshLayoutCallback = this@Adapter.swipeRefreshLayoutCallback
mFragmentList.add(fragment)
mFragmentTitleList.add(title)
}
override fun getPageTitle(position: Int): CharSequence? {
override fun getPageTitle(position: Int): CharSequence {
return mFragmentTitleList[position]
}
}

View File

@ -171,12 +171,10 @@ public class MessagesListFragment extends LazyFragment {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (b.emailList.canScrollVertically(-1)) {
if (getSwipeRefreshLayoutCallback() != null)
getSwipeRefreshLayoutCallback().invoke(false);
setSwipeToRefresh(false);
}
if (!b.emailList.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
if (getSwipeRefreshLayoutCallback() != null)
getSwipeRefreshLayoutCallback().invoke(true);
setSwipeToRefresh(true);
}
}
});

View File

@ -1,24 +1,33 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-19.
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui
package pl.szczodrzynski.edziennik.ui.modules.template
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.TemplateListItemBinding
import pl.szczodrzynski.edziennik.onClick
import kotlin.coroutines.CoroutineContext
class TemplateAdapter(
val context: Context,
val activity: AppCompatActivity,
val onItemClick: ((item: TemplateItem) -> Unit)? = null,
val onItemButtonClick: ((item: TemplateItem) -> Unit)? = null
) : RecyclerView.Adapter<TemplateAdapter.ViewHolder>() {
) : RecyclerView.Adapter<TemplateAdapter.ViewHolder>(), CoroutineScope {
private val app by lazy { context.applicationContext as App }
private val app = activity.applicationContext as App
// optional: place the manager here
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
var items = listOf<TemplateItem>()

View File

@ -1,8 +1,8 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-8.
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.dialogs
package pl.szczodrzynski.edziennik.ui.modules.template
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity

View File

@ -1,4 +1,8 @@
package pl.szczodrzynski.edziennik.ui.modules.base
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import android.os.Bundle
import android.view.LayoutInflater
@ -10,7 +14,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.databinding.FragmentTemplateBinding
import pl.szczodrzynski.edziennik.databinding.TemplateFragmentBinding
import kotlin.coroutines.CoroutineContext
class TemplateFragment : Fragment(), CoroutineScope {
@ -20,7 +24,7 @@ class TemplateFragment : Fragment(), CoroutineScope {
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentTemplateBinding
private lateinit var b: TemplateFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
@ -32,7 +36,7 @@ class TemplateFragment : Fragment(), CoroutineScope {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = FragmentTemplateBinding.inflate(inflater)
b = TemplateFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root
}
@ -41,6 +45,15 @@ class TemplateFragment : Fragment(), CoroutineScope {
if (!isAdded)
return
val pagerAdapter = TemplatePagerAdapter(
fragmentManager ?: return,
b.refreshLayout
)
b.viewPager.apply {
offscreenPageLimit = 1
adapter = pagerAdapter
currentItem = 4
b.tabLayout.setupWithViewPager(this)
}
}
}

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import androidx.fragment.app.FragmentManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyPagerAdapter
class TemplatePagerAdapter(fragmentManager: FragmentManager, swipeRefreshLayout: SwipeRefreshLayout) : LazyPagerAdapter(fragmentManager, swipeRefreshLayout) {
override fun getPage(position: Int) = TemplatePagerFragment()
override fun getPageTitle(position: Int) = "Page $position"
override fun getCount() = 10
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.databinding.TemplatePagerFragmentBinding
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import kotlin.coroutines.CoroutineContext
class TemplatePagerFragment : LazyFragment(), CoroutineScope {
companion object {
private const val TAG = "TemplatePagerFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: TemplatePagerFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = TemplatePagerFragmentBinding.inflate(inflater)
return b.root
}
override fun onPageCreated(): Boolean {
b.text.text = "Fragment $position"
b.button.addOnCheckedChangeListener { button, isChecked ->
setSwipeToRefresh(isChecked)
}
return true
}
}

View File

@ -16,7 +16,7 @@ class TimetablePagerAdapter(
private val items: List<Date>,
private val startHour: Int,
private val endHour: Int
) : LazyPagerAdapter(fragmentManager) {
) : LazyPagerAdapter(fragmentManager, null) {
companion object {
private const val TAG = "TimetablePagerAdapter"
}
@ -25,9 +25,8 @@ class TimetablePagerAdapter(
private val weekStart by lazy { today.weekStart }
private val weekEnd by lazy { weekStart.clone().stepForward(0, 0, 6) }
override fun getItem(position: Int): LazyFragment {
override fun getPage(position: Int): LazyFragment {
return TimetableDayFragment().apply {
swipeRefreshLayoutCallback = this@TimetablePagerAdapter.swipeRefreshLayoutCallback
arguments = Bundle().apply {
putInt("date", items[position].value)
putInt("startHour", startHour)
@ -40,7 +39,7 @@ class TimetablePagerAdapter(
return items.size
}
override fun getPageTitle(position: Int): CharSequence? {
override fun getPageTitle(position: Int): CharSequence {
val date = items[position]
val pageTitle = StringBuilder(Week.getFullDayName(date.weekDay))
if (date > weekEnd || date < weekStart) {

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorSurface_6dp"
app:tabIndicatorColor="?colorPrimary"
app:tabMode="auto"
app:tabSelectedTextColor="?colorPrimary"
app:tabTextColor="?android:textColorPrimary" />
<pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="Fragment 1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:checkable="true"
android:text="Enable/disable Swipe to refresh"/>
</LinearLayout>
</layout>

View File

@ -1278,4 +1278,5 @@
<string name="registration_enable_dialog_title">Rejestracja na serwerze</string>
<string name="registration_enable_dialog_text">Rejestracja jest automatyczna, jeśli ta opcja jest włączona. Pozwala na tworzenie i odbieranie wydarzeń udostępnionych innym uczniom z Twojej klasy. Dzięki temu, można dodawać do dziennika pozycje nie zapisane przez nauczyciela.\n\nUpewnij się, że zapoznałeś się z warunkami <a href="http://szkolny.eu/privacy-policy">Polityki prywatności</a> i akceptujesz jej postanowienia.</string>
<string name="menu_add_remove_cards">Dodaj lub usuń karty</string>
<string name="menu_template">Template</string>
</resources>