class BottomNavigationDrawerFragment: BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_bottomsheet, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
navigation_view.setNavigationItemSelectedListener { menuItem ->
// Bottom Navigation Drawer menu item clicks
when (menuItem!!.itemId) {
R.id.nav1 -> context!!.toast("you clicked one")
R.id.nav2 -> context!!.toast("you clicked two")
R.id.nav3 -> context!!.toast("you clicked three")
}
true
}
}
// This is an extension method for easy Toast call
fun Context.toast(message: CharSequence) {
val toast = Toast.makeText(this, message, Toast.LENGTH_SHORT)
toast.setGravity(Gravity.BOTTOM, 0, 600)
toast.show()
}
}
What i want to achieve is after clicking navigation icon in bottom app bar a modal bottom sheet is created and and a navigation drawer is shown in it.In above code i kept three items in it.Up to here everything is okay but when comes to handling item clicking part then the line:
navigation_view.setNavigationItemSelectedListener { menuItem ->
shows error. It tells :
unresolved type:setNavigationItemSelectedListener
And unresolved type in:menuItem
Here is fragment_bottomsheet.xml :
<android.support.design.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.navigation.NavigationView
android:id="#+id/navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="#menu/bottom_nav_drawer_menu"/>
</android.support.design.widget.ConstraintLayout>
What is wrong in am doing here?
It seems like issue comes from Context.toast(message: CharSequence) which you could use anko for that:
implementation "org.jetbrains.anko:anko:0.10.6"
Anyways, you can check the output by:
Toast.makeText(context, "Clicked", Toast.LENGTH_LONG).show()
And you'll see that it will work.
UPDATE:
Try using this inside your BottomNavigationDrawerFragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
navigation_view.setNavigationItemSelectedListener { menuItem ->
// Bottom Navigation Drawer menu item clicks
when (menuItem.itemId) {
R.id.home ->{
Toast.makeText(context, "Clicked", Toast.LENGTH_LONG).show()
}
// R.id.nav1 -> context!!.toast(getString(R.string.nav1_clicked))
}
// Add code here to update the UI based on the item selected
// For example, swap UI fragments here
true
}
close_imageview.setOnClickListener {
this.dismiss()
}
disableNavigationViewScrollbars(navigation_view)
}
The below method is worked for me.
class BottomNavigationDrawerFragment : BottomSheetDialogFragment() {
private val TAG: String = ImageChooserDialog::class.simpleName.toString()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.bottom_sheet_image_chooser, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<NavigationView>(R.id.navigation_view)
.setNavigationItemSelectedListener {
when(it.itemId){
R.id.action_camera -> Log.e(TAG, "action_camera")
R.id.action_gallery -> Log.e(TAG, "action_gallery")
}
return#setNavigationItemSelectedListener true
}
}
}
Related
Problem
I've been trying out Leanback's Page/Row fragment Example. As per the sample, we can use BrowseSupportFragment.FragmentFactory to show fragments when a header item gets focused. It works well, but I need to do some customization.
Question
How do I load the fragment only when the header is clicked, but not when focused?
How can I insert a custom header item without a row/page, so that I can launch a new activity when it is clicked?
My proposition of solving this problem:
TLDR; It is possible but, with some fragment nesting.
You just need to add middle layer for child fragment and open it when middle layer fragment is focused.
Fragment factory -> proxyFocusableFragment -> targetFragment
Create layout in fragment_focusable_proxy.xml with fragment container for target fragment:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/proxy_root"
android:focusable="true"
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/proxy_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
It is very important to add flag android:focusable="true" to root view tag, otherwise you will not be able to catch focus in this fragment and side menu would "bounce back".
Create proxy fragment class with focus changer listener:
class FocusableProxyFragment : Fragment(), BrowseSupportFragment.MainFragmentAdapterProvider {
private val adapter by lazy { BrowseSupportFragment.MainFragmentAdapter(this) }
override fun getMainFragmentAdapter(): BrowseSupportFragment.MainFragmentAdapter<*> {
return adapter
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_proxy_focusable, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnFocusChangeListener { _, hasFocus ->
//todo: here add target fragment and remove focus on middle layer
}
}
}
Then, inside focus listener implementation you need to remove this listener and set proxy layer not focusable, otherwise your target fragment will not work properly with leanback:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
removeProxyFocus(view)
//todo: add fragment to container
}
}
}
private fun removeProxyFocus(view: View) {
val rootView = view.findViewById<ViewGroup>(R.id.proxy_root)
rootView.focusable = View.NOT_FOCUSABLE
view.onFocusChangeListener = null
}
Add target fragment to proxy container using childFragmentManager:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
removeProxyFocus(view)
instantiateTargetFragment()
}
}
}
private fun instantiateTargetFragment() {
childFragmentManager
.beginTransaction()
.add(R.id.proxy_fragment_container, TargetFragment())
.commit()
}
Whole fragment class:
class FocusableProxyFragment : Fragment(), BrowseSupportFragment.MainFragmentAdapterProvider {
private val adapter by lazy { BrowseSupportFragment.MainFragmentAdapter(this) }
override fun getMainFragmentAdapter(): BrowseSupportFragment.MainFragmentAdapter<*> {
return adapter
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_proxy_focusable, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
removeProxyFocus(view)
instantiateTargetFragment()
}
}
}
private fun instantiateTargetFragment() {
childFragmentManager
.beginTransaction()
.add(R.id.proxy_fragment_container, TargetFragment())
.addToBackStack("cutv")
.commit()
}
private fun removeProxyFocus(view: View) {
val rootView = view.findViewById<ViewGroup>(R.id.proxy_root)
rootView.focusable = View.NOT_FOCUSABLE
view.onFocusChangeListener = null
}
}
And at the end in your fragment factory replace original target fragment with proxy.
I have a simple android application, In my application, there is one image icon, and when I click the icon, I want to show context menu. My code is debugging but when I click the image button, nothing change, so context menu not work, I do not know where is the problem, Any idea will be appreciated.
MenuFragment:
class MenuFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_menu, container, false)
popupMenu()
}
private fun popupMenu() {
val popupMenu = PopupMenu(requireContext(), menu)
popupMenu.inflate(R.menu.menu)
popupMenu.setOnMenuItemClickListener {
when(it.itemId) {
R.id.menu_one -> {
Toast.makeText(requireContext(), "menu1", Toast.LENGTH_SHORT).show()
true
}
R.id.menu_two -> {
Toast.makeText(requireContext(), "menu2", Toast.LENGTH_SHORT).show()
true
}
else -> true
}
}
menu.setOnLongClickListener {
try {
val popup = PopupMenu::class.java.getDeclaredField("mPopup")
popup.isAccessible = true
val menu = popup.get(popupMenu)
menu.javaClass
.getDeclaredMethod("setForceShowIcon",Boolean::class.java)
.invoke(menu, true)
}catch(e: Exception) {
e.printStackTrace()
}finally {
popupMenu.show()
}
true
}
}
}
fragment_menu:
<ImageView
android:id="#+id/menu"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:src="#drawable/ic_menu"
android:text="5:33 PM"
android:textSize="12sp"
/>
menu:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/menu_one"
android:title="Menu1"
android:icon="#drawable/ic_menu_one"
/>
<item
android:id="#+id/menu_two"
android:title="Menu2"
android:icon="#drawable/ic_menu_two"
/>
It seems you want to show the menu as soon as possible, since the function invocation is after the return statement then it never happen.
You have to leverage the fragment lifecycle. Override onViewCreated and call it there
override... onViewCreated(...) {
super....
popUpMenu()
}
The method onViewCreated happens immediately after the view is created. If you need it when the view is ready to interact onStart. In both cases, make sure backward navigation doesn't show it again by mistake.
0. Problem
This question by gave me the idea to implement the Data Binding Library for the purpose of opening a Link in a Webview-Fragment on click of a Button in a Bottom Sheet Fragment.
I was able to implement the Data Binding as seen in the other Question (Link), but the WebView doesn't load the new URL when a Button is clicked in said Bottom Sheet Fragment. I get the feedback from the console that the Button was clicked and the LiveData was changed though.
So, I thought the WebView does reload automatically when the LiveData changes but that doesn't seem to be the case...so I do not see my Error and not sure if I implemented everything correctly.
Hopefully someone can help me.
1. Respective Classes
1.1. WebViewFragment
class WebviewFragment : Fragment() {
private lateinit var webView: WebView
companion object {
fun newInstance() = WebviewFragment()
}
private lateinit var viewModel: WebViewViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentMainWebviewBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_main_webview, container, false)
viewModel = ViewModelProvider(this).get(WebViewViewModel::class.java)
binding.webViewModel = viewModel
binding.lifecycleOwner = this
return binding.root
}
#SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
webView = webViewMain
webView.settings.javaScriptEnabled = true
}
}
1.2. WebViewModel
class WebViewViewModel : ViewModel() {
val webViewUrl = MutableLiveData<String>().apply{ value = "file:///android_asset/html_files/gallery_page.html" }
companion object WebViewUrlLoader {
#BindingAdapter("loadUrl")
#JvmStatic
fun WebView.setUrl(url: String) {
this.loadUrl(url)
}
}
}
1.3. WebView Layout (XML)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="webViewModel"
type="com.example.ui.main.webview.WebViewViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/main_screen_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainFragmentGalleryView"
>
<WebView
android:id="#+id/webViewMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loadUrl="#{webViewModel.webViewUrl}"
android:paddingBottom="52dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
1.4. BottomSheet Fragment
class BottomSheetFragment : BottomSheetDialogFragment() {
private var fragmentView: View? = null
private lateinit var viewModel: WebViewViewModel
companion object {
fun newInstance() = BottomSheetFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
fragmentView = inflater.inflate(R.layout.view_modal_bottom_sheet, container, false)
return fragmentView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(WebViewViewModel::class.java)
initView()
}
override fun getTheme(): Int {
return R.style.Theme_NoWiredStrapInNavigationBar
}
private val mBottomSheetBehaviorCallback: BottomSheetCallback = object : BottomSheetCallback() {
var isBottomSheetUp = false
override fun onSlide(bottomSheet: View, slideOffset: Float) {
//TODO("Not yet implemented")
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
isBottomSheetUp = false
dismiss()
} else isBottomSheetUp = true
}
}
override fun setupDialog(dialog: Dialog, style: Int) {
//super.setupDialog(dialog, style)
val contentView =
View.inflate(context,
R.layout.view_modal_bottom_sheet, null)
dialog.setContentView(contentView)
val layoutParams =
(contentView.parent as View).layoutParams as CoordinatorLayout.LayoutParams
val behavior = layoutParams.behavior
if (behavior != null && behavior is BottomSheetBehavior<*>) {
behavior.setBottomSheetCallback(mBottomSheetBehaviorCallback)
}
}
private fun initView() {
action_my_pictures.setOnClickListener {
viewModel.webViewUrl.value = "https://www.google.com/"
Log.d("BottomSheet", "Button 1 Clicked ${viewModel.webViewUrl.value}")
}
action_favorites.setOnClickListener {
viewModel.webViewUrl.value = "https://www.hotmail.de/"
Log.d("BottomSheet", "Button 2 Clicked ${viewModel.webViewUrl.value}")
}
action_ranking.setOnClickListener {
viewModel.webViewUrl.value = "https://amazon.com/"
Log.d("BottomSheet", "Button 3 Clicked ${viewModel.webViewUrl.value}")
}
action_hall_of_fame.setOnClickListener {
viewModel.webViewUrl.value = "https://m.daum.net/"
Log.d("BottomSheet", "Button 4 Clicked ${viewModel.webViewUrl.value}")
}
action_liked_pictures.setOnClickListener {
viewModel.webViewUrl.value = "https://m.nate.com/"
Log.d("BottomSheet", "Button 5 Clicked ${viewModel.webViewUrl.value}")
}
action_events.setOnClickListener {
viewModel.webViewUrl.value = "https://www.danawa.com/"
Log.d("BottomSheet", "Button 6 Clicked ${viewModel.webViewUrl.value}")
}
action_close_bottom_sheet.setOnClickListener {
dismiss()
}
}
}
1.5. BottomSheet Layout (XML)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="webViewModel"
type="com.example.ui.main.webview.WebViewViewModel" />
</data>
<LinearLayout
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/bottomNavigationViewBackground"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="auto"
app:layout_behavior="#string/bottom_sheet_behavior"
app:behavior_fitToContents="true">
[7 Image/Icons as Buttons]
Example:
<LinearLayout
android:id="#+id/action_my_pictures"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
android:orientation="vertical"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="#+id/icon_my_pictures"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_horizontal"
android:src="#drawable/ic_user_color" />
<TextView
android:id="#+id/icon_my_pictures_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:singleLine="true"
android:text="My Gallery"
android:paddingTop="6dp"
android:textAlignment="center"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</layout>
2. LOG output (for Button Clicks)
D/BottomSheet: Button 1 Clicked https://www.google.com/
D/BottomSheet: Button 2 Clicked https://www.hotmail.de/
D/BottomSheet: Button 3 Clicked https://amazon.com/
D/BottomSheet: Button 4 Clicked https://m.daum.net/
D/BottomSheet: Button 4 Clicked https://m.daum.net/
D/BottomSheet: Button 5 Clicked https://m.nate.com/
D/BottomSheet: Button 6 Clicked https://www.danawa.com/
A lot of thanks in advance.
I think problem is in scope of life that you provide for viewModel.
Currently you code looks like this:
viewModel = ViewModelProvider(this).get(WebViewViewModel::class.java)
This code is invoked inside two different fragments, and because ViewModelProvider is initialized with current instance of fragment (this). Your view model is only available in this scope (fragment scope). So at the end for each fragment you will get new ViewModel. To share ViewModel between fragments you should use different approach:
Instantiate ViewModelProvider with activity. (ViewModel will be share across all fragments inside activity)
Instantiate ViewModelProvider with parentFragment. (ViewModel will be shared across all child fragments)
sample:
viewModel = ViewModelProvider(requireActivity()).get(WebViewViewModel::class.java)
viewModel = ViewModelProvider(parentFragment).get(WebViewViewModel::class.java)
please also check:
How to scope ViewModels properly?
https://developer.android.com/reference/android/arch/lifecycle/ViewModelProviders
I have a fragment that is called from the activity_main screen when a main menu drop down option is clicked. I am trying to have the fragment close and show the activity_main screen again when the cancel button is clicked. I have user input in the activity_main screen that I would like to still be there if the fragment is closed while clicking cancel but at this point i will settle for the fragment just closing and the activity_main showing back at it's original state.
I am using androidx and all of the answers I have found so far have been compatible with android.support.v7
This is the code that i have in my fragment, it does not do anything at all when I click the 'Cancel' button, the fragment just continues to sit there over the activity_main.
class SaveFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.savefragment, container, false)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
b_cancel.setOnClickListener {
fragmentManager?.popBackStack()
}
}
}
Here is the code that calls the savefragment from the MainActivity:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.i_save -> {
val SaveFragment = SaveFragment()
supportFragmentManager.beginTransaction().replace(R.id.activity_main_main, SaveFragment).commit()
true
}
R.id.i_recall -> {
val RecallFragment = RecallFragment()
supportFragmentManager.beginTransaction().replace(R.id.activity_main_main, RecallFragment).commit()
true
}
else -> super.onOptionsItemSelected(item)
}
}
Any help at all in Kotlin would be appreciated.
I have a pretty straightforward case. I want to implement shared element transition between an item in recyclerView and fragment. I'm using android navigation component in my app.
There is an article about shared transition on developer.android and topic on stackoverflow but this solution works only for view that located in fragment layout that starts transition and doesn't work for items from RecyclerView. Also there is a lib on github but i don't want to rely on 3rd party libs and do it by myself.
Is there some solution for this? Maybe it should work and this is just a bug? But I haven't found any information about it.
code sample:
transition start
class TransitionStartFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_transition_start, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val testData = listOf("one", "two", "three")
val adapter = TestAdapter(testData, View.OnClickListener { transitionWithTextViewInRecyclerViewItem(it) })
val recyclerView = view.findViewById<RecyclerView>(R.id.test_list)
recyclerView.adapter = adapter
val button = view.findViewById<Button>(R.id.open_transition_end_fragment)
button.setOnClickListener { transitionWithTextViewInFragment() }
}
private fun transitionWithTextViewInFragment(){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(transition_start_text to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
private fun transitionWithTextViewInRecyclerViewItem(view: View){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(view to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/transition_start_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/open_transition_end_fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="#id/transition_start_text"
android:text="open transition end fragment" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/test_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/open_transition_end_fragment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
adapter for recyclerView
class TestAdapter(
private val items: List<String>,
private val onItemClickListener: View.OnClickListener
) : RecyclerView.Adapter<TestAdapter.ViewHodler>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHodler {
return ViewHodler(LayoutInflater.from(parent.context).inflate(R.layout.item_test, parent, false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHodler, position: Int) {
val item = items[position]
holder.transitionText.text = item
holder.itemView.setOnClickListener { onItemClickListener.onClick(holder.transitionText) }
}
class ViewHodler(itemView: View) : RecyclerView.ViewHolder(itemView) {
val transitionText = itemView.findViewById<TextView>(R.id.item_test_text)
}
}
in onItemClick I pass the textView form item in recyclerView for transition
transition end
class TransitionEndFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setUpTransition()
return inflater.inflate(R.layout.fragment_transition_end, container, false)
}
private fun setUpTransition(){
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/transition_end_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
fun transitionWithTextViewInFragment() - has transition.
fun transitionWithTextViewInRecyclerViewItem(view: View) - no transition.
To solve the return transition problem you need to add this lines on the Source Fragment (the fragment with the recycler view) where you initialize your recycler view
// your recyclerView
recyclerView.apply {
...
adapter = myAdapter
postponeEnterTransition()
viewTreeObserver
.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
Here is my example with RecyclerView that have fragment shared transition.
In my adapter i am setting different transition name for each item based on position(In my example it is ImageView).
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.itemView.txtView.text=item
ViewCompat.setTransitionName(holder.itemView.imgViewIcon, "Test_$position")
holder.setClickListener(object : ViewHolder.ClickListener {
override fun onClick(v: View, position: Int) {
when (v.id) {
R.id.linearLayout -> listener.onClick(item, holder.itemView.imgViewIcon, position)
}
}
})
}
And when clicking on item, my interface that implemented in source fragment:
override fun onClick(text: String, img: ImageView, position: Int) {
val action = MainFragmentDirections.actionMainFragmentToSecondFragment(text, position)
val extras = FragmentNavigator.Extras.Builder()
.addSharedElement(img, ViewCompat.getTransitionName(img)!!)
.build()
NavHostFragment.findNavController(this#MainFragment).navigate(action, extras)
}
And in my destination fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
info("onCreate")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
info("onCreateView")
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
info("onViewCreated")
val name=SecondFragmentArgs.fromBundle(arguments).name
val position=SecondFragmentArgs.fromBundle(arguments).position
txtViewName.text=name
ViewCompat.setTransitionName(imgViewSecond, "Test_$position")
}
Faced the same issue as many on SO with the return transition but for me the root cause of the problem was that Navigation currently only uses replace for fragment transactions and it caused my recycler in the start fragment to reload every time you hit back which was a problem by itself.
So by solving the second (root) problem the return transition started to work without delayed animations. For those of you who are looking to keep the initial state when hitting back here is what I did :
just adding a simple check in onCreateView as so
private lateinit var binding: FragmentSearchResultsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return if (::binding.isInitialized) {
binding.root
} else {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
with(binding) {
//doing some stuff here
root
}
}
So triple win here: recycler is not redrawn, no refetching from server and also return transitions are working as expected.
I have managed return transitions to work.
Actually this is not a bug in Android and not a problem with setReorderingAllowed = true. What happens here is the original fragment (to which we return) trying to start transition before its views/data are settled up.
To fix this we have to use postponeEnterTransition() and startPostponedEnterTransition().
For example:
Original fragment:
class FragmentOne : Fragment(R.layout.f1) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
val items = listOf("one", "two", "three", "four", "five")
.zip(listOf(Color.RED, Color.GRAY, Color.GREEN, Color.BLUE, Color.YELLOW))
.map { Item(it.first, it.second) }
val rv = view.findViewById<RecyclerView>(R.id.rvItems)
rv.adapter = ItemsAdapter(items) { item, view -> navigateOn(item, view) }
view.doOnPreDraw { startPostponedEnterTransition() }
}
private fun navigateOn(item: Item, view: View) {
val extras = FragmentNavigatorExtras(view to "yura")
findNavController().navigate(FragmentOneDirections.toTwo(item), extras)
}
}
Next fragment:
class FragmentTwo : Fragment(R.layout.f2) {
val item: Item by lazy { arguments?.getSerializable("item") as Item }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
val tv = view.findViewById<TextView>(R.id.tvItemId)
with(tv) {
text = item.id
transitionName = "yura"
setBackgroundColor(item.color)
}
}
}
For more details and deeper explanation see:
https://issuetracker.google.com/issues/118475573
and
https://chris.banes.dev/2018/02/18/fragmented-transitions/
Android material design library contains MaterialContainerTransform class which allows to easily implement container transitions including transitions on recycler-view items. See container transform section for more details.
Here's an example of such a transition:
// FooListFragment.kt
class FooListFragment : Fragment() {
...
private val itemListener = object : FooListener {
override fun onClick(item: Foo, itemView: View) {
...
val transitionName = getString(R.string.foo_details_transition_name)
val extras = FragmentNavigatorExtras(itemView to transitionName)
navController.navigate(directions, extras)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Postpone enter transitions to allow shared element transitions to run.
// https://github.com/googlesamples/android-architecture-components/issues/495
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }
...
}
// FooDetailsFragment.kt
class FooDetailsFragment : Fragment() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = MaterialContainerTransform().apply {
duration = 1000
}
}
}
And don't forget to add unique transition names to the views:
<!-- foo_list_item.xml -->
<LinearLayout ...
android:transitionName="#{#string/foo_item_transition_name(foo.id)}">...</LinearLayout>
<!-- fragment_foo_details.xml -->
<LinearLayout ...
android:transitionName="#string/foo_details_transition_name">...</LinearLayout>
<!-- strings.xml -->
<resources>
...
<string name="foo_item_transition_name" translatable="false">foo_item_transition_%1$s</string>
<string name="foo_details_transition_name" translatable="false">foo_details_transition</string>
</resources>
The full sample is available on GitHub.
You can also take a look at Reply - an official android material sample app where a similar transition is implemented, see HomeFragment.kt & EmailFragment.kt. There's a codelab describing the process of implementing transitions in the app, and a video tutorial.