From bea5098cc1fb794abc19592d1d46a80630f236c0 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 13 Mar 2021 21:27:20 +0100 Subject: [PATCH] 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 --- app/build.gradle | 1 + .../tusky/AccountsInListFragment.kt | 189 ++++++++---------- .../keylesspalace/tusky/ViewMediaActivity.kt | 3 + .../conversation/ConversationsFragment.kt | 27 +-- .../fragment/InstanceListFragment.kt | 46 +++-- .../report/fragments/ReportDoneFragment.kt | 36 ++-- .../report/fragments/ReportNoteFragment.kt | 49 ++--- .../fragments/ReportStatusesFragment.kt | 39 ++-- .../fragments/SearchAccountsFragment.kt | 4 +- .../search/fragments/SearchFragment.kt | 37 ++-- .../fragments/SearchStatusesFragment.kt | 9 +- .../tusky/fragment/AccountListFragment.kt | 40 ++-- .../tusky/fragment/AccountMediaFragment.kt | 85 ++++---- .../tusky/fragment/ViewImageFragment.kt | 60 +++--- .../tusky/fragment/ViewVideoFragment.kt | 74 ++++--- .../tusky/util/ViewBindingExtensions.kt | 56 +++++- app/src/main/res/layout/fragment_search.xml | 1 - app/src/main/res/layout/fragment_timeline.xml | 5 +- 18 files changed, 412 insertions(+), 349 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9ab2647c3..583abb11d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index f1c3d54dd..0151e070c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -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 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(AccountDiffer) { + inner class Adapter : ListAdapter>(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 { + 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, 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,57 +206,58 @@ class AccountsInListFragment : DialogFragment(), Injectable { return oldItem.second == newItem.second && oldItem.first.deepEquals(newItem.first) } - } - inner class SearchAdapter : ListAdapter(SearchDiffer) { + inner class SearchAdapter : ListAdapter>(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 { + val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val holder = BindingHolder(binding) - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val (account, inAList) = getItem(position) - holder.bind(account, inAList) - - } - - 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 { - contentDescription = if (inAList) { - setImageResource(R.drawable.ic_reject_24dp) - getString(R.string.action_remove_from_list) - } else { - setImageResource(R.drawable.ic_plus_24dp) - getString(R.string.action_add_to_list) - } - } - } - - init { - acceptButton.hide() - rejectButton.setOnClickListener(this) - } - - override fun onClick(v: View?) { - val (account, inAList) = getItem(adapterPosition) + binding.notificationTextView.hide() + binding.acceptButton.hide() + binding.rejectButton.setOnClickListener { + val (account, inAList) = getItem(holder.adapterPosition) if (inAList) { onRemoveFromList(account.id) } else { onAddToList(account) } } + + return holder + } + + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val (account, inAList) = 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) + + holder.binding.rejectButton.apply { + contentDescription = if (inAList) { + setImageResource(R.drawable.ic_reject_24dp) + getString(R.string.action_remove_from_list) + } else { + setImageResource(R.drawable.ic_plus_24dp) + getString(R.string.action_add_to_list) + } + } + } + } + + 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 } } } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 4d5a124cf..86205b29f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index abae8702b..009c62f61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -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() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt index 093fc42d3..005432d88 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -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, response: Response) { 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, 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) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 03bd8ef91..794cb287b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -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() + else { + 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() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index b933b2fa7..b47b586a5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -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() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 8ffe243e5..01a12c23c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -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) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt index c453f97c5..8715e1ab2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt @@ -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() { override fun createAdapter(): PagedListAdapter { - val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) + val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) return SearchAccountsAdapter( this, @@ -46,5 +45,4 @@ class SearchAccountsFragment : SearchFragment() { companion object { fun newInstance() = SearchAccountsFragment() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index 83eb30910..32475c78c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -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 : Fragment(R.layout.fragment_search), @@ -32,6 +32,8 @@ abstract class SearchFragment : 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 @@ -48,8 +50,8 @@ abstract class SearchFragment : 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 : 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 : 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 : 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 : 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() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index 6d96bb5aa..f2ea85c0d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -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>(), StatusActionListener { @@ -78,7 +77,7 @@ class SearchStatusesFragment : SearchFragment, *> { - 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 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 } } } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index b85a87f31..f66791920 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -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() 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>> { 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>) { 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" + } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt index fc4dfcb92..c68cfb5f5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -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, 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() } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt index 33d8f192c..a0912837d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -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 + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt index 9558b03f9..5fa80fcd4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt @@ -1,15 +1,67 @@ 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 */ inline fun AppCompatActivity.viewBinding( - crossinline bindingInflater: (LayoutInflater) -> T + crossinline bindingInflater: (LayoutInflater) -> T ) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater(layoutInflater) -} \ No newline at end of file +} + +class FragmentViewBindingDelegate( + val fragment: Fragment, + val viewBindingFactory: (View) -> T +) : ReadOnlyProperty { + 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 Fragment.viewBinding(viewBindingFactory: (View) -> T) = + FragmentViewBindingDelegate(this, viewBindingFactory) diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 8c993755b..5a5953ceb 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/layout/fragment_timeline.xml b/app/src/main/res/layout/fragment_timeline.xml index 79f7fdb37..d3e716d6b 100644 --- a/app/src/main/res/layout/fragment_timeline.xml +++ b/app/src/main/res/layout/fragment_timeline.xml @@ -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" /> + \ No newline at end of file