migrating to ViewBinding part 4: Fragments (#2108)

* migrating to ViewBinding part 4: Fragment

* fix imports

* don't use viewBinding extension in ViewImage and ViewVideoFragment

* don't use viewBinding extension in ViewImage and ViewVideoFragment
This commit is contained in:
Konrad Pozniak 2021-03-13 21:27:20 +01:00 committed by GitHub
parent fbb0b11d83
commit bea5098cc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 412 additions and 349 deletions

View File

@ -125,6 +125,7 @@ dependencies {
implementation "androidx.emoji:emoji-appcompat:1.1.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.paging:paging-runtime-ktx:2.1.2"

View File

@ -23,11 +23,13 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account
@ -38,9 +40,6 @@ import com.keylesspalace.tusky.viewmodel.State
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_accounts_in_list.*
import kotlinx.android.synthetic.main.item_follow_request.*
import java.io.IOException
import javax.inject.Inject
@ -48,23 +47,11 @@ private typealias AccountInfo = Pair<Account, Boolean>
class AccountsInListFragment : DialogFragment(), Injectable {
companion object {
private const val LIST_ID_ARG = "listId"
private const val LIST_NAME_ARG = "listName"
@JvmStatic
fun newInstance(listId: String, listName: String): AccountsInListFragment {
val args = Bundle().apply {
putString(LIST_ID_ARG, listId)
putString(LIST_NAME_ARG, listName)
}
return AccountsInListFragment().apply { arguments = args }
}
}
@Inject
lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: AccountsInListViewModel
private val viewModel: AccountsInListViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentAccountsInListBinding::bind)
private lateinit var listId: String
private lateinit var listName: String
@ -79,7 +66,6 @@ class AccountsInListFragment : DialogFragment(), Injectable {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
viewModel = viewModelFactory.create(AccountsInListViewModel::class.java)
val args = requireArguments()
listId = args.getString(LIST_ID_ARG)!!
listName = args.getString(LIST_NAME_ARG)!!
@ -100,12 +86,11 @@ class AccountsInListFragment : DialogFragment(), Injectable {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
accountsRecycler.layoutManager = LinearLayoutManager(view.context)
accountsRecycler.adapter = adapter
binding.accountsRecycler.layoutManager = LinearLayoutManager(view.context)
binding.accountsRecycler.adapter = adapter
accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context)
accountsSearchRecycler.adapter = searchAdapter
binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context)
binding.accountsSearchRecycler.adapter = searchAdapter
viewModel.state
.observeOn(AndroidSchedulers.mainThread())
@ -114,15 +99,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
when (state.accounts) {
is Either.Right -> messageView.hide()
is Either.Right -> binding.messageView.hide()
is Either.Left -> handleError(state.accounts.value)
}
setupSearchView(state)
}
searchView.isSubmitButtonEnabled = true
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
binding.searchView.isSubmitButtonEnabled = true
binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.search(query ?: "")
return true
@ -141,30 +126,30 @@ class AccountsInListFragment : DialogFragment(), Injectable {
private fun setupSearchView(state: State) {
if (state.searchResult == null) {
searchAdapter.submitList(listOf())
accountsSearchRecycler.hide()
accountsRecycler.show()
binding.accountsSearchRecycler.hide()
binding.accountsRecycler.show()
} else {
val listAccounts = state.accounts.asRightOrNull() ?: listOf()
val newList = state.searchResult.map { acc ->
acc to listAccounts.contains(acc)
}
searchAdapter.submitList(newList)
accountsSearchRecycler.show()
accountsRecycler.hide()
binding.accountsSearchRecycler.show()
binding.accountsRecycler.hide()
}
}
private fun handleError(error: Throwable) {
messageView.show()
binding.messageView.show()
val retryAction = { _: View ->
messageView.hide()
binding.messageView.hide()
viewModel.load(listId)
}
if (error is IOException) {
messageView.setup(R.drawable.elephant_offline,
binding.messageView.setup(R.drawable.elephant_offline,
R.string.error_network, retryAction)
} else {
messageView.setup(R.drawable.elephant_error,
binding.messageView.setup(R.drawable.elephant_error,
R.string.error_generic, retryAction)
}
}
@ -187,39 +172,28 @@ class AccountsInListFragment : DialogFragment(), Injectable {
}
}
inner class Adapter : ListAdapter<Account, Adapter.ViewHolder>(AccountDiffer) {
inner class Adapter : ListAdapter<Account, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_follow_request, parent, false)
return ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = BindingHolder(binding)
binding.notificationTextView.hide()
binding.acceptButton.hide()
binding.rejectButton.setOnClickListener {
onRemoveFromList(getItem(holder.adapterPosition).id)
}
binding.rejectButton.contentDescription =
binding.root.context.getString(R.string.action_remove_from_list)
return holder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener, LayoutContainer {
override val containerView = itemView
init {
acceptButton.hide()
rejectButton.setOnClickListener(this)
rejectButton.contentDescription =
itemView.context.getString(R.string.action_remove_from_list)
}
fun bind(account: Account) {
displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis)
usernameTextView.text = account.username
loadAvatar(account.avatar, avatar, radius, animateAvatar)
}
override fun onClick(v: View?) {
onRemoveFromList(getItem(adapterPosition).id)
}
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
val account = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
holder.binding.usernameTextView.text = account.username
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
}
}
@ -232,34 +206,36 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return oldItem.second == newItem.second
&& oldItem.first.deepEquals(newItem.first)
}
}
inner class SearchAdapter : ListAdapter<AccountInfo, SearchAdapter.ViewHolder>(SearchDiffer) {
inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(SearchDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_follow_request, parent, false)
return ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = BindingHolder(binding)
binding.notificationTextView.hide()
binding.acceptButton.hide()
binding.rejectButton.setOnClickListener {
val (account, inAList) = getItem(holder.adapterPosition)
if (inAList) {
onRemoveFromList(account.id)
} else {
onAddToList(account)
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
return holder
}
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
val (account, inAList) = getItem(position)
holder.bind(account, inAList)
}
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
holder.binding.usernameTextView.text = account.username
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener, LayoutContainer {
override val containerView = itemView
fun bind(account: Account, inAList: Boolean) {
displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis)
usernameTextView.text = account.username
loadAvatar(account.avatar, avatar, radius, animateAvatar)
rejectButton.apply {
holder.binding.rejectButton.apply {
contentDescription = if (inAList) {
setImageResource(R.drawable.ic_reject_24dp)
getString(R.string.action_remove_from_list)
@ -269,20 +245,19 @@ class AccountsInListFragment : DialogFragment(), Injectable {
}
}
}
init {
acceptButton.hide()
rejectButton.setOnClickListener(this)
}
override fun onClick(v: View?) {
val (account, inAList) = getItem(adapterPosition)
if (inAList) {
onRemoveFromList(account.id)
} else {
onAddToList(account)
}
}
companion object {
private const val LIST_ID_ARG = "listId"
private const val LIST_NAME_ARG = "listName"
@JvmStatic
fun newInstance(listId: String, listName: String): AccountsInListFragment {
val args = Bundle().apply {
putString(LIST_ID_ARG, listId)
putString(LIST_NAME_ARG, listName)
}
return AccountsInListFragment().apply { arguments = args }
}
}
}

View File

@ -69,6 +69,9 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
private val binding by viewBinding(ActivityViewMediaBinding::inflate)
val toolbar: View
get() = binding.toolbar
var isToolbarVisible = true
private set

View File

@ -28,6 +28,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.keylesspalace.tusky.AccountActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment
@ -38,7 +39,7 @@ import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.fragment_timeline.*
import com.keylesspalace.tusky.util.viewBinding
import javax.inject.Inject
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
@ -48,6 +49,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentTimelineBinding::bind)
private lateinit var adapter: ConversationAdapter
private var layoutManager: LinearLayoutManager? = null
@ -73,14 +76,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
binding.recyclerView.layoutManager = layoutManager
binding.recyclerView.adapter = adapter
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
progressBar.hide()
statusView.hide()
binding.progressBar.hide()
binding.statusView.hide()
initSwipeToRefresh()
@ -97,16 +100,16 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
private fun initSwipeToRefresh() {
viewModel.refreshState.observe(viewLifecycleOwner) {
swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
}
swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.refresh()
}
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
}
private fun onTopLoaded() {
recyclerView.scrollToPosition(0)
binding.recyclerView.scrollToPosition(0)
}
override fun onReblog(reblog: Boolean, position: Int) {
@ -183,7 +186,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
private fun jumpToTop() {
if (isAdded) {
layoutManager?.scrollToPosition(0)
recyclerView.stopScroll()
binding.recyclerView.stopScroll()
}
}

View File

@ -2,9 +2,7 @@ package com.keylesspalace.tusky.components.instancemute.fragment
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration
@ -14,16 +12,17 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener
import com.keylesspalace.tusky.databinding.FragmentInstanceListBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_instance_list.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -31,9 +30,12 @@ import java.io.IOException
import javax.inject.Inject
class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
@Inject
lateinit var api: MastodonApi
private val binding by viewBinding(FragmentInstanceListBinding::bind)
private var fetching = false
private var bottomId: String? = null
private var adapter = DomainMutesAdapter(this)
@ -42,12 +44,12 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.setHasFixedSize(true)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
binding.recyclerView.adapter = adapter
val layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager
binding.recyclerView.layoutManager = layoutManager
scrollListener = object : EndlessOnScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
@ -57,7 +59,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
}
}
recyclerView.addOnScrollListener(scrollListener)
binding.recyclerView.addOnScrollListener(scrollListener)
fetchInstances()
}
@ -85,7 +87,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
override fun onResponse(call: Call<Any>, response: Response<Any>) {
if (response.isSuccessful) {
adapter.removeItem(position)
Snackbar.make(recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG)
Snackbar.make(binding.recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) {
mute(true, instance, position)
}
@ -103,10 +105,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
return
}
fetching = true
instanceProgressBar.show()
binding.instanceProgressBar.show()
if (id != null) {
recyclerView.post { adapter.bottomLoading = true }
binding.recyclerView.post { adapter.bottomLoading = true }
}
api.domainBlocks(id, bottomId)
@ -116,7 +118,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
val instances = response.body()
if (response.isSuccessful && instances != null) {
onFetchInstancesSuccess(instances, response.headers().get("Link"))
onFetchInstancesSuccess(instances, response.headers()["Link"])
} else {
onFetchInstancesFailure(Exception(response.message()))
}
@ -127,7 +129,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
adapter.bottomLoading = false
instanceProgressBar.hide()
binding.instanceProgressBar.hide()
val links = HttpHeaderLink.parse(linkHeader)
val next = HttpHeaderLink.findByRelationType(links, "next")
@ -137,32 +139,32 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
fetching = false
if (adapter.itemCount == 0) {
messageView.show()
messageView.setup(
binding.messageView.show()
binding.messageView.setup(
R.drawable.elephant_friend_empty,
R.string.message_empty,
null
)
} else {
messageView.hide()
binding.messageView.hide()
}
}
private fun onFetchInstancesFailure(throwable: Throwable) {
fetching = false
instanceProgressBar.hide()
binding.instanceProgressBar.hide()
Log.e(TAG, "Fetch failure", throwable)
if (adapter.itemCount == 0) {
messageView.show()
binding.messageView.show()
if (throwable is IOException) {
messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
messageView.hide()
binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
binding.messageView.hide()
this.fetchInstances(null)
}
} else {
messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
messageView.hide()
binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
binding.messageView.hide()
this.fetchInstances(null)
}
}

View File

@ -22,12 +22,13 @@ import androidx.fragment.app.activityViewModels
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.databinding.FragmentReportDoneBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.fragment_report_done.*
import com.keylesspalace.tusky.util.viewBinding
import javax.inject.Inject
class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
@ -37,8 +38,10 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportDoneBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName)
binding.textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName)
handleClicks()
subscribeObservables()
}
@ -46,14 +49,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
private fun subscribeObservables() {
viewModel.muteState.observe(viewLifecycleOwner) {
if (it !is Loading) {
buttonMute.show()
progressMute.show()
binding.buttonMute.show()
binding.progressMute.show()
} else {
buttonMute.hide()
progressMute.hide()
binding.buttonMute.hide()
binding.progressMute.hide()
}
buttonMute.setText(when (it.data) {
binding.buttonMute.setText(when (it.data) {
true -> R.string.action_unmute
else -> R.string.action_mute
})
@ -61,14 +64,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
viewModel.blockState.observe(viewLifecycleOwner) {
if (it !is Loading) {
buttonBlock.show()
progressBlock.show()
binding.buttonBlock.show()
binding.progressBlock.show()
}
else {
buttonBlock.hide()
progressBlock.hide()
binding.buttonBlock.hide()
binding.progressBlock.hide()
}
buttonBlock.setText(when (it.data) {
binding.buttonBlock.setText(when (it.data) {
true -> R.string.action_unblock
else -> R.string.action_block
})
@ -77,13 +80,13 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
}
private fun handleClicks() {
buttonDone.setOnClickListener {
binding.buttonDone.setOnClickListener {
viewModel.navigateTo(Screen.Finish)
}
buttonBlock.setOnClickListener {
binding.buttonBlock.setOnClickListener {
viewModel.toggleBlock()
}
buttonMute.setOnClickListener {
binding.buttonMute.setOnClickListener {
viewModel.toggleMute()
}
}
@ -91,5 +94,4 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
companion object {
fun newInstance() = ReportDoneFragment()
}
}

View File

@ -24,10 +24,10 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.databinding.FragmentReportNoteBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_report_note.*
import java.io.IOException
import javax.inject.Inject
@ -38,6 +38,8 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportNoteBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
fillViews()
handleChanges()
@ -46,29 +48,29 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
}
private fun handleChanges() {
editNote.doAfterTextChanged {
binding.editNote.doAfterTextChanged {
viewModel.reportNote = it?.toString() ?: ""
}
checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked ->
binding.checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked ->
viewModel.isRemoteNotify = isChecked
}
}
private fun fillViews() {
editNote.setText(viewModel.reportNote)
binding.editNote.setText(viewModel.reportNote)
if (viewModel.isRemoteAccount){
checkIsNotifyRemote.show()
reportDescriptionRemoteInstance.show()
binding.checkIsNotifyRemote.show()
binding.reportDescriptionRemoteInstance.show()
}
else{
checkIsNotifyRemote.hide()
reportDescriptionRemoteInstance.hide()
binding.checkIsNotifyRemote.hide()
binding.reportDescriptionRemoteInstance.hide()
}
if (viewModel.isRemoteAccount)
checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer)
checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify
binding.checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer)
binding.checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify
}
private fun subscribeObservables() {
@ -83,13 +85,13 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
}
private fun showError(error: Throwable?) {
editNote.isEnabled = true
checkIsNotifyRemote.isEnabled = true
buttonReport.isEnabled = true
buttonBack.isEnabled = true
progressBar.hide()
binding.editNote.isEnabled = true
binding.checkIsNotifyRemote.isEnabled = true
binding.buttonReport.isEnabled = true
binding.buttonBack.isEnabled = true
binding.progressBar.hide()
Snackbar.make(buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG)
Snackbar.make(binding.buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG)
.apply {
setAction(R.string.action_retry) {
sendReport()
@ -103,19 +105,19 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
}
private fun showLoading() {
buttonReport.isEnabled = false
buttonBack.isEnabled = false
editNote.isEnabled = false
checkIsNotifyRemote.isEnabled = false
progressBar.show()
binding.buttonReport.isEnabled = false
binding.buttonBack.isEnabled = false
binding.editNote.isEnabled = false
binding.checkIsNotifyRemote.isEnabled = false
binding.progressBar.show()
}
private fun handleClicks() {
buttonBack.setOnClickListener {
binding.buttonBack.setOnClickListener {
viewModel.navigateTo(Screen.Back)
}
buttonReport.setOnClickListener {
binding.buttonReport.setOnClickListener {
sendReport()
}
}
@ -123,5 +125,4 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
companion object {
fun newInstance() = ReportNoteFragment()
}
}

View File

@ -34,6 +34,7 @@ import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.components.report.adapter.AdapterHandler
import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter
import com.keylesspalace.tusky.databinding.FragmentReportStatusesBinding
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
@ -44,8 +45,8 @@ import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject
class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler {
@ -58,6 +59,8 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportStatusesBinding::bind)
private lateinit var adapter: StatusesAdapter
private var snackbarErrorRetry: Snackbar? = null
@ -93,9 +96,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
}
private fun setupSwipeRefreshLayout() {
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.setOnRefreshListener {
snackbarErrorRetry?.dismiss()
viewModel.refreshStatuses()
}
@ -118,10 +121,10 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
adapter = StatusesAdapter(statusDisplayOptions,
viewModel.statusViewState, this)
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
binding.recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
viewModel.statuses.observe(viewLifecycleOwner) {
adapter.submitList(it)
@ -129,9 +132,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
viewModel.networkStateAfter.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
progressBarBottom.show()
binding.progressBarBottom.show()
else
progressBarBottom.hide()
binding.progressBarBottom.hide()
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg)
@ -139,22 +142,22 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
viewModel.networkStateBefore.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
progressBarTop.show()
binding.progressBarTop.show()
else
progressBarTop.hide()
binding.progressBarTop.hide()
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg)
}
viewModel.networkStateRefresh.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing)
progressBarLoading.show()
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !binding.swipeRefreshLayout.isRefreshing)
binding.progressBarLoading.show()
else
progressBarLoading.hide()
binding.progressBarLoading.hide()
if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING)
swipeRefreshLayout.isRefreshing = false
binding.swipeRefreshLayout.isRefreshing = false
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg)
}
@ -162,7 +165,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
if (snackbarErrorRetry?.isShown != true) {
snackbarErrorRetry = Snackbar.make(swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE)
snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE)
snackbarErrorRetry?.setAction(R.string.action_retry) {
viewModel.retryStatusLoad()
}
@ -172,11 +175,11 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private fun handleClicks() {
buttonCancel.setOnClickListener {
binding.buttonCancel.setOnClickListener {
viewModel.navigateTo(Screen.Back)
}
buttonContinue.setOnClickListener {
binding.buttonContinue.setOnClickListener {
viewModel.navigateTo(Screen.Note)
}
}

View File

@ -23,11 +23,10 @@ import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.NetworkState
import kotlinx.android.synthetic.main.fragment_search.*
class SearchAccountsFragment : SearchFragment<Account>() {
override fun createAdapter(): PagedListAdapter<Account, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
return SearchAccountsAdapter(
this,
@ -46,5 +45,4 @@ class SearchAccountsFragment : SearchFragment<Account>() {
companion object {
fun newInstance() = SearchAccountsFragment()
}
}

View File

@ -17,11 +17,11 @@ import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.components.search.SearchViewModel
import com.keylesspalace.tusky.databinding.FragmentSearchBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_search.*
import javax.inject.Inject
abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
@ -32,6 +32,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory }
protected val binding by viewBinding(FragmentSearchBinding::bind)
private var snackbarErrorRetry: Snackbar? = null
abstract fun createAdapter(): PagedListAdapter<T, *>
@ -48,8 +50,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
}
private fun setupSwipeRefreshLayout() {
swipeRefreshLayout.setOnRefreshListener(this)
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
binding.swipeRefreshLayout.setOnRefreshListener(this)
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
}
private fun subscribeObservables() {
@ -59,7 +61,7 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
networkStateRefresh.observe(viewLifecycleOwner) {
searchProgressBar.visible(it == NetworkState.LOADING)
binding.searchProgressBar.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) {
showError()
@ -69,7 +71,7 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
networkState.observe(viewLifecycleOwner) {
progressBarBottom.visible(it == NetworkState.LOADING)
binding.progressBarBottom.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) {
showError()
@ -82,24 +84,25 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
}
private fun initAdapter() {
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context)
binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)
adapter = createAdapter()
searchRecyclerView.adapter = adapter
searchRecyclerView.setHasFixedSize(true)
(searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
binding.searchRecyclerView.adapter = adapter
binding.searchRecyclerView.setHasFixedSize(true)
(binding.searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
private fun showNoData(isEmpty: Boolean) {
if (isEmpty && networkStateRefresh.value == NetworkState.LOADED)
searchNoResultsText.show()
else
searchNoResultsText.hide()
if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) {
binding.searchNoResultsText.show()
} else {
binding.searchNoResultsText.hide()
}
}
private fun showError() {
if (snackbarErrorRetry?.isShown != true) {
snackbarErrorRetry = Snackbar.make(layoutRoot, R.string.failed_search, Snackbar.LENGTH_INDEFINITE)
snackbarErrorRetry = Snackbar.make(binding.root, R.string.failed_search, Snackbar.LENGTH_INDEFINITE)
snackbarErrorRetry?.setAction(R.string.action_retry) {
snackbarErrorRetry = null
viewModel.retryAllSearches()
@ -122,8 +125,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
override fun onRefresh() {
// Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins.
swipeRefreshLayout.post {
swipeRefreshLayout.isRefreshing = false
binding.swipeRefreshLayout.post {
binding.swipeRefreshLayout.isRefreshing = false
}
viewModel.retryAllSearches()
}

View File

@ -63,7 +63,6 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_search.*
class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
@ -78,7 +77,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
get() = super.adapter as SearchStatusesAdapter
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
@ -91,12 +90,11 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
)
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context)
binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)
return SearchStatusesAdapter(statusDisplayOptions, this)
}
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
searchAdapter.getItem(position)?.let {
viewModel.contentHiddenChange(it, isShowing)
@ -486,5 +484,4 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
.show()
}
}
}

View File

@ -31,6 +31,7 @@ import com.keylesspalace.tusky.AccountListActivity.Type
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.*
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship
@ -40,12 +41,12 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_account_list.*
import retrofit2.Response
import java.io.IOException
import java.util.*
@ -56,6 +57,8 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
@Inject
lateinit var api: MastodonApi
private val binding by viewBinding(FragmentAccountListBinding::bind)
private lateinit var type: Type
private var id: String? = null
@ -73,12 +76,12 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.setHasFixedSize(true)
binding.recyclerView.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
binding.recyclerView.layoutManager = layoutManager
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
val pm = PreferenceManager.getDefaultSharedPreferences(view.context)
val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
@ -90,7 +93,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis)
else -> FollowAdapter(this, animateAvatar, animateEmojis)
}
recyclerView.adapter = adapter
binding.recyclerView.adapter = adapter
scrollListener = object : EndlessOnScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
@ -101,7 +104,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
}
}
recyclerView.addOnScrollListener(scrollListener)
binding.recyclerView.addOnScrollListener(scrollListener)
fetchAccounts()
}
@ -136,7 +139,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
val unmutedUser = mutesAdapter.removeItem(position)
if (unmutedUser != null) {
Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG)
Snackbar.make(binding.recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) {
mutesAdapter.addItem(unmutedUser, position)
onMute(true, id, position, notifications)
@ -180,7 +183,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
val unblockedUser = blocksAdapter.removeItem(position)
if (unblockedUser != null) {
Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG)
Snackbar.make(binding.recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) {
blocksAdapter.addItem(unblockedUser, position)
onBlock(true, id, position)
@ -260,7 +263,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
fetching = true
if (fromId != null) {
recyclerView.post { adapter.setBottomLoading(true) }
binding.recyclerView.post { adapter.setBottomLoading(true) }
}
getFetchCallByListType(fromId)
@ -303,14 +306,14 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
fetching = false
if (adapter.itemCount == 0) {
messageView.show()
messageView.setup(
binding.messageView.show()
binding.messageView.setup(
R.drawable.elephant_friend_empty,
R.string.message_empty,
null
)
} else {
messageView.hide()
binding.messageView.hide()
}
}
@ -339,15 +342,15 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
Log.e(TAG, "Fetch failure", throwable)
if (adapter.itemCount == 0) {
messageView.show()
binding.messageView.show()
if (throwable is IOException) {
messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
messageView.hide()
binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
binding.messageView.hide()
this.fetchAccounts(null)
}
} else {
messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
messageView.hide()
binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
binding.messageView.hide()
this.fetchAccounts(null)
}
}
@ -368,5 +371,4 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
}
}
}
}

View File

@ -30,6 +30,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
@ -39,13 +40,13 @@ import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.SquareImageView
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_timeline.*
import retrofit2.Response
import java.io.IOException
import java.util.*
@ -58,49 +59,36 @@ import javax.inject.Inject
*/
class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable {
companion object {
@JvmStatic
fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
val fragment = AccountMediaFragment()
val args = Bundle()
args.putString(ACCOUNT_ID_ARG, accountId)
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh)
fragment.arguments = args
return fragment
}
private const val ACCOUNT_ID_ARG = "account_id"
private const val TAG = "AccountMediaFragment"
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"
}
private var isSwipeToRefreshEnabled: Boolean = true
private var needToRefresh = false
@Inject
lateinit var api: MastodonApi
private val binding by viewBinding(FragmentTimelineBinding::bind)
private lateinit var accountId: String
private val adapter = MediaGridAdapter()
private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING
private lateinit var accountId: String
private var isSwipeToRefreshEnabled: Boolean = true
private var needToRefresh = false
private val callback = object : SingleObserver<Response<List<Status>>> {
override fun onError(t: Throwable) {
fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) {
swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE
topProgressBar?.hide()
statusView.show()
binding.swipeRefreshLayout.isRefreshing = false
binding.progressBar.visibility = View.GONE
binding.topProgressBar.hide()
binding.statusView.show()
if (t is IOException) {
statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
doInitialLoadingIfNeeded()
}
} else {
statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
doInitialLoadingIfNeeded()
}
}
@ -112,9 +100,9 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
override fun onSuccess(response: Response<List<Status>>) {
fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) {
swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE
topProgressBar?.hide()
binding.swipeRefreshLayout.isRefreshing = false
binding.progressBar.visibility = View.GONE
binding.topProgressBar.hide()
val body = response.body()
body?.let { fetched ->
@ -126,11 +114,11 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
}
adapter.addTop(result)
if (result.isNotEmpty())
recyclerView.scrollToPosition(0)
binding.recyclerView.scrollToPosition(0)
if (statuses.isEmpty()) {
statusView.show()
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
binding.statusView.show()
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
}
}
}
@ -181,18 +169,18 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
binding.recyclerView.layoutManager = layoutManager
binding.recyclerView.adapter = adapter
if (isSwipeToRefreshEnabled) {
swipeRefreshLayout.setOnRefreshListener {
binding.swipeRefreshLayout.setOnRefreshListener {
refresh()
}
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
}
statusView.visibility = View.GONE
binding.statusView.visibility = View.GONE
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
@ -216,7 +204,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
}
private fun refresh() {
statusView.hide()
binding.statusView.hide()
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
if (statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING
@ -229,12 +217,12 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
.subscribe(callback)
if (!isSwipeToRefreshEnabled)
topProgressBar?.show()
binding.topProgressBar.show()
}
private fun doInitialLoadingIfNeeded() {
if (isAdded) {
statusView.hide()
binding.statusView.hide()
}
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING
@ -344,4 +332,19 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
needToRefresh = true
}
companion object {
@JvmStatic
fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
val fragment = AccountMediaFragment()
val args = Bundle()
args.putString(ACCOUNT_ID_ARG, accountId)
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh)
fragment.arguments = args
return fragment
}
private const val ACCOUNT_ID_ARG = "account_id"
private const val TAG = "AccountMediaFragment"
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"
}
}

View File

@ -32,13 +32,12 @@ import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.github.chrisbanes.photoview.PhotoViewAttacher
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewImageBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible
import io.reactivex.subjects.BehaviorSubject
import kotlinx.android.synthetic.main.activity_view_media.*
import kotlinx.android.synthetic.main.fragment_view_image.*
import kotlin.math.abs
class ViewImageFragment : ViewMediaFragment() {
@ -48,6 +47,9 @@ class ViewImageFragment : ViewMediaFragment() {
fun onPhotoTap()
}
private var _binding: FragmentViewImageBinding? = null
private val binding get() = _binding!!
private lateinit var attacher: PhotoViewAttacher
private lateinit var photoActionsListener: PhotoActionsListener
private lateinit var toolbar: View
@ -71,18 +73,19 @@ class ViewImageFragment : ViewMediaFragment() {
description: String?,
showingDescription: Boolean
) {
photoView.transitionName = url
mediaDescription.text = description
captionSheet.visible(showingDescription)
binding.photoView.transitionName = url
binding.mediaDescription.text = description
binding.captionSheet.visible(showingDescription)
startedTransition = false
loadImageFromNetwork(url, previewUrl, photoView)
loadImageFromNetwork(url, previewUrl, binding.photoView)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = requireActivity().toolbar
toolbar = (requireActivity() as ViewMediaActivity).toolbar
this.transition = BehaviorSubject.create()
return inflater.inflate(R.layout.fragment_view_image, container, false)
_binding = FragmentViewImageBinding.inflate(inflater, container, false)
return binding.root
}
@SuppressLint("ClickableViewAccessibility")
@ -105,7 +108,7 @@ class ViewImageFragment : ViewMediaFragment() {
}
}
attacher = PhotoViewAttacher(photoView).apply {
attacher = PhotoViewAttacher(binding.photoView).apply {
// This prevents conflicts with ViewPager
setAllowParentInterceptOnEdge(true)
@ -127,7 +130,7 @@ class ViewImageFragment : ViewMediaFragment() {
var lastY = 0f
photoView.setOnTouchListener { v, event ->
binding.photoView.setOnTouchListener { v, event ->
// This part is for scaling/translating on vertical move.
// We use raw coordinates to get the correct ones during scaling
@ -140,11 +143,11 @@ class ViewImageFragment : ViewMediaFragment() {
val diff = event.rawY - lastY
// This code is to prevent transformations during page scrolling
// If we are already translating or we reached the threshold, then transform.
if (photoView.translationY != 0f || abs(diff) > 40) {
photoView.translationY += (diff)
val scale = (-abs(photoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
photoView.scaleY = scale
photoView.scaleX = scale
if (binding.photoView.translationY != 0f || abs(diff) > 40) {
binding.photoView.translationY += (diff)
val scale = (-abs(binding.photoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
binding.photoView.scaleY = scale
binding.photoView.scaleX = scale
lastY = event.rawY
return@setOnTouchListener true
}
@ -158,13 +161,13 @@ class ViewImageFragment : ViewMediaFragment() {
}
private fun onGestureEnd() {
if (photoView == null) {
if (_binding == null) {
return
}
if (abs(photoView.translationY) > 180) {
if (abs(binding.photoView.translationY) > 180) {
photoActionsListener.onDismiss()
} else {
photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
binding.photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
}
}
@ -173,15 +176,17 @@ class ViewImageFragment : ViewMediaFragment() {
}
override fun onToolbarVisibilityChange(visible: Boolean) {
if (photoView == null || !userVisibleHint || captionSheet == null) {
if (_binding == null || !userVisibleHint ) {
return
}
isDescriptionVisible = showingDescription && visible
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
captionSheet.animate().alpha(alpha)
binding.captionSheet.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
captionSheet?.visible(isDescriptionVisible)
if (_binding != null) {
binding.captionSheet.visible(isDescriptionVisible)
}
animation.removeListener(this)
}
})
@ -189,8 +194,9 @@ class ViewImageFragment : ViewMediaFragment() {
}
override fun onDestroyView() {
Glide.with(this).clear(photoView)
Glide.with(this).clear(binding.photoView)
transition.onComplete()
_binding = null
super.onDestroyView()
}
@ -253,7 +259,7 @@ class ViewImageFragment : ViewMediaFragment() {
photoActionsListener.onBringUp()
}
// Hide progress bar only on fail request from internet
if (!isCacheRequest) progressBar?.hide()
if (!isCacheRequest && _binding != null) binding.progressBar.hide()
// We don't want to overwrite preview with null when main image fails to load
return !isCacheRequest
}
@ -261,14 +267,16 @@ class ViewImageFragment : ViewMediaFragment() {
@SuppressLint("CheckResult")
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>,
dataSource: DataSource, isFirstResource: Boolean): Boolean {
progressBar?.hide() // Always hide the progress bar on success
if (_binding != null) {
binding.progressBar.hide() // Always hide the progress bar on success
}
if (!startedTransition || !shouldStartTransition) {
// Set this right away so that we don't have to concurrent post() requests
startedTransition = true
// post() because load() replaces image with null. Sometimes after we set
// the thumbnail.
photoView.post {
binding.photoView.post {
target.onResourceReady(resource, null)
if (shouldStartTransition) photoActionsListener.onBringUp()
}

View File

@ -26,16 +26,18 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.MediaController
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView
import kotlinx.android.synthetic.main.activity_view_media.*
import kotlinx.android.synthetic.main.fragment_view_video.*
class ViewVideoFragment : ViewMediaFragment() {
private var _binding: FragmentViewVideoBinding? = null
private val binding get() = _binding!!
private lateinit var toolbar: View
private val handler = Handler(Looper.getMainLooper())
private val hideToolbar = Runnable {
@ -52,7 +54,7 @@ class ViewVideoFragment : ViewMediaFragment() {
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
// Start/pause/resume video playback as fragment is shown/hidden
super.setUserVisibleHint(isVisibleToUser)
if (videoView == null) {
if (_binding == null) {
return
}
@ -60,10 +62,10 @@ class ViewVideoFragment : ViewMediaFragment() {
if (mediaActivity.isToolbarVisible) {
handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS)
}
videoView.start()
binding.videoView.start()
} else {
handler.removeCallbacks(hideToolbar)
videoView.pause()
binding.videoView.pause()
mediaController.hide()
}
}
@ -75,11 +77,11 @@ class ViewVideoFragment : ViewMediaFragment() {
description: String?,
showingDescription: Boolean
) {
mediaDescription.text = description
mediaDescription.visible(showingDescription)
binding.mediaDescription.text = description
binding.mediaDescription.visible(showingDescription)
videoView.transitionName = url
videoView.setVideoPath(url)
binding.videoView.transitionName = url
binding.videoView.setVideoPath(url)
mediaController = object : MediaController(mediaActivity) {
override fun show(timeout: Int) {
// We're doing manual auto-close management.
@ -100,10 +102,10 @@ class ViewVideoFragment : ViewMediaFragment() {
}
}
mediaController.setMediaPlayer(videoView)
videoView.setMediaController(mediaController)
videoView.requestFocus()
videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener {
mediaController.setMediaPlayer(binding.videoView)
binding.videoView.setMediaController(mediaController)
binding.videoView.requestFocus()
binding.videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener {
override fun onPause() {
handler.removeCallbacks(hideToolbar)
}
@ -117,31 +119,31 @@ class ViewVideoFragment : ViewMediaFragment() {
}
}
})
videoView.setOnPreparedListener { mp ->
val containerWidth = videoContainer.measuredWidth.toFloat()
val containerHeight = videoContainer.measuredHeight.toFloat()
binding.videoView.setOnPreparedListener { mp ->
val containerWidth = binding.videoContainer.measuredWidth.toFloat()
val containerHeight = binding.videoContainer.measuredHeight.toFloat()
val videoWidth = mp.videoWidth.toFloat()
val videoHeight = mp.videoHeight.toFloat()
if(containerWidth/containerHeight > videoWidth/videoHeight) {
videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
binding.videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
} else {
videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
binding.videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
binding.videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
}
// Wait until the media is loaded before accepting taps as we don't want toolbar to
// be hidden until then.
videoView.setOnTouchListener { _, _ ->
binding.videoView.setOnTouchListener { _, _ ->
mediaActivity.onPhotoTap()
false
}
progressBar.hide()
binding.progressBar.hide()
mp.isLooping = true
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) {
videoView.start()
binding.videoView.start()
}
}
@ -155,9 +157,10 @@ class ViewVideoFragment : ViewMediaFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = requireActivity().toolbar
mediaActivity = activity as ViewMediaActivity
return inflater.inflate(R.layout.fragment_view_video, container, false)
toolbar = mediaActivity.toolbar
_binding = FragmentViewVideoBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -174,7 +177,7 @@ class ViewVideoFragment : ViewMediaFragment() {
}
override fun onToolbarVisibilityChange(visible: Boolean) {
if (videoView == null || mediaDescription == null || !userVisibleHint) {
if (_binding == null || !userVisibleHint) {
return
}
@ -182,20 +185,22 @@ class ViewVideoFragment : ViewMediaFragment() {
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
if (isDescriptionVisible) {
// If to be visible, need to make visible immediately and animate alpha
mediaDescription.alpha = 0.0f
mediaDescription.visible(isDescriptionVisible)
binding.mediaDescription.alpha = 0.0f
binding.mediaDescription.visible(isDescriptionVisible)
}
mediaDescription.animate().alpha(alpha)
binding.mediaDescription.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
mediaDescription?.visible(isDescriptionVisible)
if (_binding != null) {
binding.mediaDescription.visible(isDescriptionVisible)
}
animation.removeListener(this)
}
})
.start()
if (visible && videoView.isPlaying && !isAudio) {
if (visible && binding.videoView.isPlaying && !isAudio) {
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
} else {
handler.removeCallbacks(hideToolbar)
@ -204,4 +209,9 @@ class ViewVideoFragment : ViewMediaFragment() {
override fun onTransitionEnd() {
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -1,8 +1,15 @@
package com.keylesspalace.tusky.util
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c
@ -13,3 +20,48 @@ inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
) = lazy(LazyThreadSafetyMode.NONE) {
bindingInflater(layoutInflater)
}
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null
init {
fragment.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(
fragment,
{ t ->
t?.lifecycle?.addObserver(
object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
}
)
}
)
}
}
)
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = binding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
}
return viewBindingFactory(thisRef.requireView()).also { this@FragmentViewBindingDelegate.binding = it }
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutRoot"
android:layout_width="@dimen/timeline_width"
android:layout_height="match_parent">

View File

@ -32,12 +32,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"
app:layout_constrainedHeight="true" />
tools:visibility="visible" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar"
@ -50,4 +50,5 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>