I want to build searchview in center of screen and when I focus I want to move into toolbar. Remove Searchview only from toolbar when I remove focus from SearchView. I don't won't in different activities/fragments. Something similar to this view. I tried some piece of code but lack of knowledge so I didn't succeed. Can someone guide me
<androidx.coordinatorlayout.widget.CoordinatorLayout
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">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
UPDATE
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:backgroundTint="#color/orange_lighter"
android:gravity="bottom"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.SearchView
android:id="#+id/consultationSearchView"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:theme="#style/SearchViewTheme"
app:closeIcon="#drawable/ic_cancel"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:searchIcon="#drawable/ic_search" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
// more views
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Activity code
package com.example.app.consultation
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.SearchView
import com.example.app.common.BaseActivity
import com.example.app.databinding.ExploreConsultationsLayoutBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
class ExploreConsultationsActivity : BaseActivity() {
companion object {
const val CONSULTATION_COVER_LIST_KEY = "consultation_cover_list"
}
private val binding by lazy { ExploreConsultationsLayoutBinding.inflate(layoutInflater) }
val viewModel by viewModel<ExploreConsultationViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViewModel()
setContentView(binding.root)
setupView()
}
fun setupViewModel() {
viewModel.categoriesList = intent?.getParcelableArrayListExtra(CONSULTATION_COVER_LIST_KEY)
}
fun setupView() {
hideActionBar()
setupSearchView()
}
fun setupSearchView() {
binding.consultationSearchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?): Boolean {
if (newText != null) {
viewModel.removeSearchViewFocus.value = false
viewModel.queryText = newText
}
return true
}
})
setOnQueryTextFocusChangeListener { _, hasFocus ->
binding.appBar.setExpanded(!hasFocus)
isSelected = hasFocus
}
}
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if (ev?.action == MotionEvent.ACTION_DOWN) {
val view: View? = currentFocus
if (view is SearchView) {
val outRect = Rect()
view.getGlobalVisibleRect(outRect);
if (!outRect.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
view.clearFocus()
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
}
return super.dispatchTouchEvent(ev)
}
}
To achieve something similar to the gif that you attached , you can try this :
In your activity_main.xml layout file :
<androidx.coordinatorlayout.widget.CoordinatorLayout
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:focusable="true">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBar"
android:layout_width="match_parent"
android:gravity="bottom"
android:backgroundTint="#color/red_primary_80"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
android:fitsSystemWindows="true">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginStart="16dp"
app:iconifiedByDefault="false"
android:layout_marginEnd="16dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
/>
</com.google.android.material.appbar.AppBarLayout>
<!-- Scrollable content -->
</androidx.coordinatorlayout.widget.CoordinatorLayout>
In your MainActivity.java file :
public class MainActivity extends Activity {
private TextInputEditText txt;
private AppBarLayout appBarLayout;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txt = findViewById(R.id.textField);
appBarLayout = findViewById(R.id.appBar);
//This code will collapse the AppBar according to the focus on the textField
txt.setOnFocusChangeListener((v, hasFocus) -> {
appBarLayout.setExpanded(!hasFocus);
});
}
// Function to clear focus from the textfield when you touch outside the view
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
View v = getCurrentFocus();
if (v instanceof SearchView.SearchAutoComplete) {
Rect outRect = new Rect();
v.getGlobalVisibleRect(outRect);
if (!outRect.contains((int)event.getRawX(), (int)event.getRawY())) {
v.clearFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
}
}
return super.dispatchTouchEvent( event );
}
}
You can modify the layout further to achieve your desired look.
Related
Hey I am working on search bar in android. I get the lead from this post. Now I want to try something more. Above post explanation in short :- I have searchview in the middle of screen. When we focus to on searchview we animate to go to top of screen and after remove focus goes to original position of search view. Now I want to show back arrow with initial screen load, look like this
Image 1
When we focus I need to show screen like this
Image 2
I tried some piece of code, but I am not succeed
ExploreConsultationsLayoutBinding.xml
<androidx.coordinatorlayout.widget.CoordinatorLayout
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:focusable="true">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBar"
android:layout_width="match_parent"
android:gravity="bottom"
android:backgroundTint="#color/red_primary_80"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
android:fitsSystemWindows="true">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/black">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginStart="16dp"
app:iconifiedByDefault="false"
android:layout_marginEnd="16dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
/>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<!-- Scrollable content -->
</androidx.coordinatorlayout.widget.CoordinatorLayout>
ExploreConsultationsActivity.kt
package com.example.app.consultation
import android.content.Context
import android.graphics.Rect
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.SearchView
import com.example.app.common.BaseActivity
import com.example.app.databinding.ExploreConsultationsLayoutBinding
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class ExploreConsultationsActivity : BaseActivity() {
companion object {
const val CONSULTATION_LIST_KEY = "consultation_list"
}
private val binding by lazy { ExploreConsultationsLayoutBinding.inflate(layoutInflater) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupView()
}
fun setupView() {
hideActionBar()
setupSearchView()
}
fun hideActionBar() {
supportActionBar?.let { actionBar ->
actionBar.hide()
}
}
fun setupSearchView() {
binding.consultationSearchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?): Boolean {
if (newText != null) {
viewModel.queryText = newText
}
return true
}
})
setOnQueryTextFocusChangeListener { view, hasFocus ->
binding.appBar.setExpanded(!hasFocus)
if (hasFocus) {
binding.toolbar.apply {
setSupportActionBar(this)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
} else {
supportActionBar?.setDisplayHomeAsUpEnabled(false)
}
isSelected = hasFocus
}
}
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if (ev?.action == MotionEvent.ACTION_DOWN) {
val view: View? = currentFocus
if (view is SearchView.SearchAutoComplete) {
val outRect = Rect()
view.getGlobalVisibleRect(outRect);
if (!outRect.contains(ev.rawX.toInt(), ev.rawY.toInt())) {
view.clearFocus()
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
}
return super.dispatchTouchEvent(ev)
}
}
Mainfest.xml
<activity android:name="ExploreConsultationsActivity"
android:screenOrientation="portrait"
android:theme="#style/NoActionBar"/>
Style.xml
<style name="NoActionBar" parent="#style/Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">#color/abc</item>
<item name="colorPrimaryDark">#color/xyz</item>
<item name="colorAccent">#color/abc</item>
<item name="android:theme">#style/AppTheme</item>
<item name="android:colorBackground">#color/white</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:statusBarColor">#color/status_bar</item>
<item name="android:windowLightStatusBar" tools:ignore="NewApi">true</item>
</style>
Actual Output
Expected Output
Image 1 and Image 2 please look top image of question.
Github Project
UPDATE
my search view is very close to status bar so how can I give top margin or padding?
You could change the start margin of the SearchView when it got the focus; and return it to the original margin when it loses the focus:
var originalMargin = 0
fun setupSearchView() {
binding.consultationSearchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?): Boolean {
if (newText != null) {
}
return true
}
})
val params =
binding.consultationSearchView.layoutParams as CollapsingToolbarLayout.LayoutParams
originalMargin = params.marginStart
setOnQueryTextFocusChangeListener { view, hasFocus ->
binding.appBar.setExpanded(!hasFocus)
isSelected = hasFocus
if (hasFocus)
params.marginStart = originalMargin + 150 // arbitrary constant
else
params.marginStart = originalMargin
view.layoutParams = params
}
}
}
this is MainActivity
class MainActivity : AppCompatActivity(), FragmentDrawerListener,
BottomNavigationView.OnNavigationItemSelectedListener {
private lateinit var drawerFragment: DrawerFragment
#Inject
lateinit var mainActivityViewModel: MainActivityViewModel
#Inject
lateinit var appPreference: AppPreference
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidInjection.inject(this)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
navigation.setOnNavigationItemSelectedListener(this)
displayView()
}
override fun onBackPressed() {
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
drawer_layout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
override fun onDrawerItemSelected(view: View, position: Int) {
displayView(position)
}
private fun displayView() {
drawerFragment =
supportFragmentManager.findFragmentById(R.id.fragment_navigation_drawer) as DrawerFragment
drawerFragment.init(R.id.fragment_navigation_drawer, drawer_layout, toolbar)
displayView(0)
}
private fun displayView(position: Int) {
when (position) {
0 -> {
HomeFragment()
}
5 -> {
val intent = Intent(this, MerchantOnBoardingActivity::class.java)
startActivity(intent)
}
9 -> {
val intent = Intent(this, AppSecurityActivity::class.java)
startActivity(intent)
}
else -> {
val bundle = Bundle()
bundle.putInt("menuItemPosition", position - 1)
val intent = Intent(this, MenuActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
}
}
private fun showDefaultPage() {
val fragment = HomeFragment()
title = "Home"
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.main_content, fragment)
fragmentTransaction.commit()
supportActionBar?.title = title
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
var fragment: Fragment? = null
when (item.itemId) {
R.id.navigation_home -> fragment = HomeFragment()
R.id.navigation_monitor -> Toast.makeText(this, "Monitor", Toast.LENGTH_LONG).show()
R.id.navigation_profile -> Toast.makeText(this, "Profile", Toast.LENGTH_LONG).show()
}
return loadFragment(fragment)
}
private fun loadFragment(fragment: Fragment?): Boolean {
//switching fragment
if (fragment != null) {
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit()
return true
}
return false
}
}
This is my xml :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:local="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/white"
android:orientation="vertical"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<androidx.drawerlayout.widget.DrawerLayout
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
local:popupTheme="#style/ThemeOverlay.AppCompat.Light"
local:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar" />
<FrameLayout
android:id="#+id/main_content"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemIconTint="#color/design_default_color_primary"
app:itemTextColor="#color/design_default_color_primary"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="#menu/navigation" />
</LinearLayout>
<fragment
android:id="#+id/fragment_navigation_drawer"
android:name="com.egpaid.employeeapp.home.homedashboard.DrawerFragment"
android:layout_width="260dp"
android:layout_height="match_parent"
android:layout_gravity="start"
app:layout="#layout/fragment_drawer"
tools:layout="#layout/fragment_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
</LinearLayout>
</RelativeLayout>
using this code i am able to display data but when i try to click on Home button on either from bottom navigation view or from drawber then i am getting
No view found for id 0x7f0900a3 (com.egpaid.employeeapp:id/fragment_container) for fragment HomeFragment{9d6bf5} (7b531975-d1d4-4ad4-b3c7-6f43ae436c6d) id=0x7f0900a3}
I don't know what i am missing i am trying to create custom navigation drawber with bottom navigation view please help me Thanks
AppbarLayout has a big child, so we can drag and fling it.
It goes wrong when I fling appbar with its child first and fling scrollview before previous fling animation is finished.
The scrolling then became a mess when double flinging occured on both appbar and nestedscrollview
Here is the layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior"
>
<View
android:layout_width="match_parent"
android:layout_height="800dp"
android:background="#8f8"
app:layout_scrollFlags="scroll"
/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:layout_width="match_parent"
android:layout_height="800dp"
android:background="#88f" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
I have the same problem with RecyclerView
See HeaderViewBehavior.setHeaderTopBottomOffset, it's invoked with different directions for newOffset parameter.
You should cancel AppBarLayout.Behavior fling when onStartNestedScroll invoked.
How to fix:
Override AppBarLayout.Behavior
package com.google.android.material.appbar // original package
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.OverScroller
import androidx.coordinatorlayout.widget.CoordinatorLayout
class FixFlingBehavior #JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null
) : AppBarLayout.Behavior(context, attributeSet) {
private var isAppbarFlinging: Boolean = false
private var originalScroller: OverScroller? = null
private val fakeScroller: OverScroller = object : OverScroller(context) {
override fun computeScrollOffset(): Boolean {
scroller = originalScroller
return false // it cancels HeaderBehavior FlingRunnable
}
}
override fun setHeaderTopBottomOffset(coordinatorLayout: CoordinatorLayout, appBarLayout: AppBarLayout, newOffset: Int, minOffset: Int, maxOffset: Int): Int {
isAppbarFlinging = minOffset == Int.MIN_VALUE && maxOffset == Int.MAX_VALUE
if (scroller === fakeScroller) {
scroller = originalScroller
}
return super.setHeaderTopBottomOffset(coordinatorLayout, appBarLayout, newOffset, minOffset, maxOffset)
}
override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
super.onFlingFinished(parent, layout)
isAppbarFlinging = false
}
override fun onStartNestedScroll(parent: CoordinatorLayout, child: AppBarLayout, directTargetChild: View, target: View, nestedScrollAxes: Int, type: Int): Boolean {
if (isAppbarFlinging && scroller !== fakeScroller) {
originalScroller = scroller
scroller = fakeScroller
}
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
}
}
And use it in your layout
...
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.appbar.FixFlingBehavior">
...
I made a viewpager2 which has two Fragments, inside each Fragment there is a Recyclerview. The viewpager itself is inside a Nestedscrollview in order to hide the toolbar when scroll up. Here is my code:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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"
>
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appbarlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways">
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="#+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
app:tabGravity="fill"
app:tabMode="fixed"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<include layout="#layout/material_design_floating_action_menu" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
As I said the viewPager2 have two fragment each of them have a recyclerview. Here problem is, fragment 2 recyclerView take the same height of fragment 1 recyclerView though both recyclerView have different list items and their height should be depends on the list items. I mean, I am expecting these recyclerViews height should act separately based on the list. How can I solve this issue? Please let me know if you need fragment code.
Edit:
Activity code which holds the viewPager2
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initToolbar();
init();
viewPager.setAdapter(createCardAdapter());
new TabLayoutMediator(tabLayout, viewPager,
new TabLayoutMediator.TabConfigurationStrategy() {
#Override public void onConfigureTab(#NonNull TabLayout.Tab tab, int position) {
//tab.setText("Tab " + (position + 1));
if(position == 0){
tab.setText("Home");
}else if(position == 1){
tab.setText("Events");
}
}
}).attach();
RunnTimePermissions.requestForAllRuntimePermissions(this);
showNotifyDialog();
}
ViewPager Adapter Code:
public class ViewPagerAdapter extends FragmentStateAdapter {
private static final int CARD_ITEM_SIZE = 2;
public ViewPagerAdapter(#NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
}
#NonNull #Override public Fragment createFragment(int position) {
switch (position){
case 0:
return HomeFragment.newInstance("abc","abc");
// break;
case 1:
return EventListFragment.newInstance("abc","abc");
// break;
}
return HomeFragment.newInstance("abc","abc");
}
#Override public int getItemCount() {
return CARD_ITEM_SIZE;
}
}
Fragment 1 layout:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.EventListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_event_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Fragment 2 layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.MedicineListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_medicine_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Alright buddy. I did it! I have the solution to this issue.
First off, here's the official response from Google after I opened an issue.
https://issuetracker.google.com/issues/188474850?pli=1
Anyway, on to the fix. Replace the NestedScrollView with this class:
https://gist.github.com/AfzalivE/fdce03eeee8e16203bcc37ba26d7abf3
The idea is to basically create a very light-weight version of NestedScrollView. Instead of messing with child heights, we listen to the children's scroll and either let them scroll, or forward the scrolling to the BottomSheetBehavior. For example when the RecyclerView has been scrolled all the way to the top, then it doesn't consume any of the scrolling, so we can forward that to the bottom sheet so it can scroll. And we only allow the RecyclerView to scroll when the Bottom sheet is expanded.
Also, I must add that this scenario works out of the box with Jetpack Compose + Accompanist-pager so just do that if you really need perfect functionality.
class BottomSheetScrollView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs),
NestedScrollingParent2 {
private val TAG = "NestedScroll3"
private val childHelper = NestedScrollingChildHelper(this).apply {
isNestedScrollingEnabled = true
}
private var behavior: BottomSheetBehavior<*>? = null
var started = false
var canScroll = false
var pendingCanScroll = false
var dyPreScroll = 0
init {
ViewCompat.setNestedScrollingEnabled(this, true)
}
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
onNextScrollStop(newState == BottomSheetBehavior.STATE_EXPANDED)
Log.d(
"BottomSheet",
"Can scroll CHANGED to: $canScroll, because bottom sheet state is ${
getBottomSheetStateString(newState)
}"
)
}
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
}
fun onNextScrollStop(canScroll: Boolean) {
pendingCanScroll = canScroll
}
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
// ViewPager2's RecyclerView does not participate in this nested scrolling.
// This allows it to show it's overscroll indicator.
if (target is RecyclerView) {
val layoutManager = target.layoutManager as LinearLayoutManager
if (layoutManager.orientation == LinearLayoutManager.HORIZONTAL) {
target.isNestedScrollingEnabled = false
}
}
if (!started) {
Log.d(TAG, "started nested scroll from $target")
childHelper.startNestedScroll(axes, type)
started = true
}
return true
}
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
Log.d(TAG, "accepted nested scroll from $target")
}
override fun onStopNestedScroll(target: View, type: Int) {
if (started) {
childHelper.stopNestedScroll(type)
started = false
Log.d(
TAG,
"stopped nested scroll from $target, changing canScroll from $canScroll to $pendingCanScroll"
)
canScroll = pendingCanScroll
}
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
Log.d(
TAG,
"onNestedScroll: dxC: $dxConsumed, dyC: $dyConsumed, dxU: $dxUnconsumed, dyU: $dyUnconsumed"
)
if (dyUnconsumed == dyPreScroll && dyPreScroll < 0) {
canScroll = false
Log.d(TAG, "Can scroll CHANGED to: $canScroll, because scrolled to the top of the list")
}
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
Log.d(
TAG,
"onNestedPreScroll: dx: $dx, dy: $dy, consumed: [ ${consumed.joinToString(", ")} ]"
)
if (!canScroll) {
childHelper.dispatchNestedPreScroll(dx, dy, consumed, null, type)
// Ensure all dy is consumed to prevent premature scrolling when not allowed.
consumed[1] = dy
} else {
dyPreScroll = dy
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
behavior = findBottomSheetBehaviorParent(parent) as BottomSheetBehavior<*>?
behavior?.addBottomSheetCallback(bottomSheetCallback)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
behavior?.removeBottomSheetCallback(bottomSheetCallback)
}
private fun findBottomSheetBehaviorParent(parent: ViewParent?): CoordinatorLayout.Behavior<*>? {
if (parent !is View) {
throw IllegalArgumentException(
"None of this view's ancestors are associated with BottomSheetBehavior"
)
}
val layoutParams = parent.layoutParams
return if (layoutParams is CoordinatorLayout.LayoutParams && layoutParams.behavior != null) {
layoutParams.behavior
} else {
findBottomSheetBehaviorParent((parent as View).parent)
}
}
private fun getBottomSheetStateString(state: Int): String {
return when (state) {
1 -> "STATE_DRAGGING"
2 -> "STATE_SETTLING"
3 -> "STATE_EXPANDED"
4 -> "STATE_COLLAPSED"
5 -> "STATE_HIDDEN"
6 -> "STATE_HALF_EXPANDED"
else -> "0"
}
}
}
I do not understand why you have recyclerview inside FrameLayout .
Here problem is, fragment 2 recyclerView take the same height of
fragment 1 recyclerView though both recyclerView have different list
items and their height should be depends on the list items
RecyclerView generally always depends on the item_layout until we have not fixed its height n width to match_parent.
In your case you have fixed RecyclerView android:layout_width and android:layout_width to match_parent in both the Fragment .
Try this:
Fragment 1
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.EventListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_event_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
/>
</FrameLayout>
Fragment 2
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.MedicineListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_medicine_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
/>
</FrameLayout>
P.S: assuming each reacyclerView's item_layout would be having height as wrap_content
I'm using RecyclerView inside ViewPager and that ViewPager is inside NestedScrollView and the problem is scroll is lagging, its not to much but it's not perfect .
I have read Android Document about rendering performance and my onBind method takes about 1 milisecond or less and my customImageView rounding bitmap in setImageBitmap method and this method takes abut 2 milisecond.
from the document I have only 16 milisecond to draw a frame and my time is very less than 16 milisecond and I'm pretty sure that other parts not doing anyThing related to UI. so how can I detect problem and solve it??
My activity layout:
<android.support.design.widget.CoordinatorLayout 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:background="#color/gray_dark"
android:fitsSystemWindows="true"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="#+id/app_bar"
android:layout_width="match_parent"
android:layout_height="#dimen/app_bar_height"
android:fitsSystemWindows="true">
<android.support.design.widget.CollapsingToolbarLayout
android:id="#+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:minHeight="56dp"
app:contentScrim="#color/colorTransparent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:statusBarScrim="#color/colorTransparent">
<ImageView
android:id="#+id/headerImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:contentDescription="#string/header_image"
android:fitsSystemWindows="true"
android:foreground="#color/gray_alpha"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<android.support.design.widget.TabLayout
android:id="#+id/tablayout"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom"
app:layout_collapseMode="pin"
app:tabIndicatorColor="#color/pink"
app:tabMode="scrollable"
app:tabSelectedTextColor="#color/pink"
app:tabTextColor="#color/white" />
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
android:orientation="vertical">
<android.support.v4.view.ViewPager
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
and my fragment layout code:
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:id="#+id/recycler"
android:layout_height="match_parent"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
android:background="#color/white"
android:orientation="vertical">
</android.support.v7.widget.RecyclerView>
My ViewPager adapter is so simple and only attaches fragments:
class MainPagerAdapter(fm: FragmentManager, val listener: FragmentGetter) : FragmentPagerAdapter(fm) {
private val arr = SparseArray<Fragment>()
override fun getItem(position: Int): Fragment {
return when (position) {
0 -> {
if (arr[0] == null) arr.append(0, listener.getBeshnoFragment());arr[0]
}
1 -> {
if (arr[1] == null) arr.append(1, listener.getPlaylistFragment()); arr[1]
}
2 -> {
if (arr[2] == null) arr.append(2, listener.getMusicFragment())
(arr[2] as MusicFragment).load(ALL_MUSICS, null)
arr[2]
}
3 -> {
if (arr[3] == null) arr.append(3, listener.getAlbumFragment()); arr[3]
}
4 -> {
if (arr[4] == null) arr.append(4, listener.getArtistFragment()); arr[4]
}
else -> {
if (arr[5] == null) arr.append(5, listener.getFoldersFragment()); arr[5]
}
}
}
override fun getPageTitle(position: Int): CharSequence? {
return listener.getPageTitle(position)
}
override fun getCount(): Int {
return TabLayoutData.items.size
}
interface FragmentGetter {
fun getBeshnoFragment(): BeshnoFragment
fun getMusicFragment(): MusicFragment
fun getAlbumFragment(): AlbumFragment
fun getArtistFragment(): ArtistFragment
fun getPlaylistFragment(): PlaylistFragment
fun getFoldersFragment(): FoldersFragment
fun getPageTitle(pos: Int): String
}
}
and Here is my RecyclerView adapter code:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val holder = ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.music_list_item, parent, false
)
)
holder.menu.setOnClickListener {
val pos = holder.adapterPosition
if (menu != null) menu!!.dismiss()
mCurrentMusic = musics[pos]
menu = PopupMenu(holder.itemView.context, holder.menu)
buildMenu()
menu!!.show()
}
holder.itemView.setOnClickListener {
val pos = holder.adapterPosition
listener.playMusic(musics[pos])
}
holder.image.radius = UIUtils.convertDpToPixel(8f, parent.context).toInt()
return holder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val t1 = System.currentTimeMillis()
val music = musics[position]
holder.name.text = music.title
holder.album.text = music.album
val path = music.imagePath
MusicUtils.setMusicImageAsync(path, holder.image, MusicUtils.QUALITY_MED)
Log.d(javaClass.simpleName, "adapter bind time: ${System.currentTimeMillis() - t1}")//this time takes less than 1 miliseconds
}
MusicUtils.setMusicImageAsync code is:
fun setMusicImageAsync(album_ART: String?, cover: ImageView, quality: Int) {
if (album_ART == null || album_ART.isEmpty() || !File(album_ART).exists()) {
setPlaceHolder(cover)
return
}
ImageLoaderTask(quality) { // on post execute
if (it == null) {
setPlaceHolder(cover)
} else {
val t1 = System.currentTimeMillis()
cover.setImageBitmap(it)
cover.setPadding(0, 0, 0, 0)
Log.d("MusicImageLoader", "time is: ${System.currentTimeMillis() - t1}") //this time takes about 2 miliseconds
}
}.execute(album_ART)
}
also I'm using draw cache for RecyclerView but scroll is lagging.
I think this can help:
recyclerView.setNestedScrollingEnabled(false)