Lollipop AppBarLayout/Toolbar missing overscroll animation - java

using the most basic example with AppBarLayout and Toolbar, I cannot see the overscroll animation (the glow from bottom nor top) when trying to scroll more. However, if you fling the content, it will show it.
Here is the code (nav_drawer_toolbar_layout.xml):
<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">
<!-- Replace fragments in this content frame, like a RecycleView -->
<FrameLayout
android:id="#+id/content_frame"
app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:minHeight="?attr/actionBarSize"
app:titleTextAppearance="#style/Base.TextAppearance.AppCompat.Title"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"
app:layout_scrollFlags="scroll|enterAlways"/>
</android.support.design.widget.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
Followed by simple Activity class:
public class MyActivity extends AppCompatActivity implements {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.nav_drawer_toolbar_layout);
// Setup the toolbar/actionbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FragmentManager manager = getFragmentManager();
manager.beginTransaction().replace(R.id.content_frame, new MyFragmentList).commit();
}
}
MyFragmentList is a fragment with a RecycleView with content to scroll the app.
However if I remove AppBarLayout from the xml and leave Toolbar open (just comment AppBarLayout opening and closing), it will show the overscroll animation (the glow) when scrolling.
Or if you remove layout_scrollFlags="scroll" then the overscroll works but you can't get the actionbar to hide when you scroll.
For extra information, debugging RecycleView, line 2272
if(this.mBottomGlow != null && !this.mBottomGlow.isFinished()) {
is always finished when including AppBarLayout and not finished when it is not there. Is something over-writing its touch events?
Does anyone know who to show overscroll animation (glow) with AppBarLayout?

EDIT: There seems to be a ticket for this bug. You could definintely do what artur.dr...#gmail.com did and extend RecyclerView to override RecyclerView#dispatchNestedScroll to always return false (he writes true in his report) you can get overscroll animations working, though i'm pretty sure it might break something down the line.
Unfortunately how RecyclerView is coded and how NestedScrollingChild API is made there is no clean way to have the desired behavior.
This is from RecyclerView (23.1.1, however I do not believe any version before it fixes the issue) inside the method scrollByInternal.
if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
if (ev != null) {
ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
}
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
} else if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
if (ev != null) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
}
considerReleasingGlowsOnScroll(x, y);
}
As we can see here on the javadoc for dispatchNestedScroll (part of the NestedScrollingChild API) as long as there is one parent that consumes the scroll, RecyclerView will not apply any overscroll animation (edge glow).
AppBarLayout does consume scrolling, more specifically as long as there is a NestedScrollingParent that returns true on onStartNestedScroll, overscroll animations will not happen.
CoordinatorLayout is a NestedScrollingParent but does not return true unless there is a CoordinatorLayout.Behavior that does. AppBarLayout's default behavior does implement this methdo to return true when there is vertical scrolling + AppBarLayout has something to scroll + view is big enough to scroll.
// Return true if we're nested scrolling vertically, and we have scrollable children
// and the scrolling view is big enough to scroll
final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
&& child.hasScrollableChildren()
&& parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
Flinging takes a slightly different approach, allowing the overscroll animation happening regardless if the NestedScrollingParent is consuming the scrolling.
if (!dispatchNestedPreFling(velocityX, velocityY)) {
final boolean canScroll = canScrollHorizontal || canScrollVertical;
dispatchNestedFling(velocityX, velocityY, canScroll);
if (canScroll) {
velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
mViewFlinger.fling(velocityX, velocityY);
return true;
}
}
Honestly I cannot tell if this is a bug because both logic make sense. If you are scrolling to the top part of the view, and you have something akin to a CollapsingToolbar, you wouldn't want an overscoll animation to happen. However there is a way to make it so that the behavior can consume the x/y amount of scrolling to stop the animation from happening. It is also weird that both code for scrolling and flinging is different.

Related

Android RecyclerView height problem inside view pager2 inside NestedScrollView with TabLayout [duplicate]

There are a few posts on getting ViewPager to work with varying height items that center around extending ViewPager itself to modify its onMeasure to support this.
However, given that ViewPager2 is marked as a final class, extending it isn't something that we can do.
Does anyone know if there's a way to make this work out?
E.g. let's say I have two views:
View1 = 200dp
View2 = 300dp
When the ViewPager2 (layout_height="wrap_content") loads -- looking at View1, its height will be 200dp.
But when I scroll over to View2, the height is still 200dp; the last 100dp of View2 is cut off.
The solution is to register a PageChangeCallback and adjust the LayoutParams of the ViewPager2 after asking the child to re-measure itself.
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val view = // ... get the view
view.post {
val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (pager.layoutParams.height != view.measuredHeight) {
// ParentViewGroup is, for example, LinearLayout
// ... or whatever the parent of the ViewPager2 is
pager.layoutParams = (pager.layoutParams as ParentViewGroup.LayoutParams)
.also { lp -> lp.height = view.measuredHeight }
}
}
}
})
Alternatively, if your view's height can change at some point due to e.g. asynchronous data load, then use a global layout listener instead:
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
private val listener = ViewTreeObserver.OnGlobalLayoutListener {
val view = // ... get the view
updatePagerHeightForChild(view)
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val view = // ... get the view
// ... IMPORTANT: remove the global layout listener from other views
otherViews.forEach { it.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener) }
view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
}
private fun updatePagerHeightForChild(view: View) {
view.post {
val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (pager.layoutParams.height != view.measuredHeight) {
// ParentViewGroup is, for example, LinearLayout
// ... or whatever the parent of the ViewPager2 is
pager.layoutParams = (pager.layoutParams as ParentViewGroup.LayoutParams)
.also { lp -> lp.height = view.measuredHeight }
}
}
}
}
See discussion here:
https://issuetracker.google.com/u/0/issues/143095219
In my case, adding adapter.notifyDataSetChanged() in onPageSelected helped.
Just do this for the desired Fragment in ViewPager2:
override fun onResume() {
super.onResume()
layoutTaskMenu.requestLayout()
}
Jetpack: binding.root.requestLayout() (thanks #syed-zeeshan for the specifics)
Stumbled across this case myself however with fragments.
Instead of resizing the view as the accepted answer I decided to wrap the view in a ConstraintLayout. This requires you to specify a size of your ViewPager2 and not use wrap_content.
So Instead of changing size of our viewpager it will have to be minimum size of the largest view it handles.
A bit new to Android so don't know if this is a good solution or not, but it does the job for me.
In other words:
<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"
>
<!-- Adding transparency above your view due to wrap_content -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
>
<!-- Your view here -->
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
For me this worked perfectly:
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position,positionOffset,positionOffsetPixels)
if (position>0 && positionOffset==0.0f && positionOffsetPixels==0){
viewPager2.layoutParams.height =
viewPager2.getChildAt(0).height
}
}
})
Just call .requestLayout() to the root view of layout in the onResume() of your Fragment class which is being used in ViewPager2
Just Add this small code in your all fragments of ViewPager2
#Override
public void onResume() {
super.onResume();
binding.getRoot().requestLayout();
}
This is working for me perfectly (If you are not using binding then Just get a root layout instance in place of binding)
I had a similar problem and solved it as below.
In my case I had ViewPager2 working with TabLayout with fragments with different heights.
In each fragment in the onResume() method, I added the following code:
#Override
public void onResume() {
super.onResume();
setProperHeightOfView();
}
private void setProperHeightOfView() {
View layoutView = getView().findViewById( R.id.layout );
if (layoutView!=null) {
ViewGroup.LayoutParams layoutParams = layoutView.getLayoutParams();
if (layoutParams!=null) {
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutView.requestLayout();
}
}
}
R.id.layout is layout of particular fragment.
I hope I helped.
Best regards,
T.
No posted answer was entirely applicable for my case - not knowing the height of each page in advance - so I solved different ViewPager2 pages heights using ConstraintLayout in the following way:
<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"
>
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<!-- ... -->
</com.google.android.material.appbar.AppBarLayout>
<!-- Wrapping view pager into constraint layout to make it use maximum height for each page. -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/viewPagerContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/appBarLayout"
>
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/bottom_navigation_menu"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
#Mephoros code works perfectly when swiped between views but won't work when views are peeked for first time. It works as intended after swiping it.
So, swipe viewpager programmatically:
binding.viewpager.setCurrentItem(1)
binding.viewpager.setCurrentItem(0) //back to initial page
I'm using the ViewPager2ViewHeightAnimator from here
I got stuck with this problem too. I was implementing TabLayout and ViewPager2 for several tabs with account information. Those tabs had to be with different heights, for example: View1 - 300dp, View2 - 200dp, Tab3 - 500dp. The height was locked within first view's height and the others were cut or extended to (example) 300dp. Like so:
So after two days of searches nothing helped me (or i had to try better) but i gave up and used NestedScrollView for all my views. For sure, now i don't have effect, that the header of profile scrolls with info in 3 views, but at least it now works somehow.
Hope this one helps someone! If you have some advices, feel free to reply!
P.s. I'm sorry for my bad english skills.
Only adapter.notifyDataSetChanged() worked for me in ViewPager2. Used below code in Kotlin.
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
adapter.notifyDataSetChanged()
}
})
why don't you do it by replacing not using ViewPager2.
like code in below:
private void fragmentController(Fragment newFragment){
FragmentTransaction ft;
ft = mainAct.getSupportFragmentManager().beginTransaction();
ft.replace(R.id.relMaster, newFragment);
ft.addToBackStack(null);
ft.commitAllowingStateLoss();
}
Where relMaster is RelativeLayout.
Answer by #Mephoros worked for me in the end. I had a Recyclerview with pagination(v3) in one of the fragments and it was behaving really strangely with page loads. Here is a working snippet based on the answer in case anyone has problems getting and cleaning views.
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
var view : View? = null
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
view?.let {
updatePagerHeightForChild(it)
}
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
// ... IMPORTANT: remove the global layout listener from other view
view?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
view = (viewPager[0] as RecyclerView).layoutManager?.findViewByPosition(position)
view?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
}
private fun updatePagerHeightForChild(view: View) {
view.post {
val wMeasureSpec = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY)
val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (viewPager.layoutParams.height != view.measuredHeight) {
viewPager.layoutParams = (viewPager.layoutParams)
.also { lp -> lp.height = view.measuredHeight }
}
}
}
})
just add this code to each fragments :
override fun onResume() {
super.onResume()
binding.root.requestLayout()
}
Finally, I can fix this without requestLayout, notifyDataChanged, or the other solutions above!
It's really easy and simple!
You just need to save current height onPause, then load the saved height onResume.
Look at this example code:
public class MyTabbedFragment extends Fragment {
public MyTabbedFragmentViewBinding binding;
String TAG = "MyTabbedFragment";
int heightBeforePause;
// other code
#Override
public void onResume() {
super.onResume();
Log.d(TAG, "lifecycle | onResume | before set height | rec view height: " + binding.recycleView.getHeight() + " | height before pause: " + heightBeforePause);
// load the saved height
if(heightBeforePause > 0) {
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, heightBeforePause);
binding.recycleView.setLayoutParams(layoutParams);
}
}
#Override
public void onPause() {
super.onPause();
// save the current height
heightBeforePause = binding.recycleView.getHeight();
Log.d(TAG, "lifecycle | onPause | rec view height: " + binding.recycleView.getHeight());
}
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val view = (viewPager[0] as RecyclerView).layoutManager?.findViewByPosition(position)
view?.post {
val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (viewPager.layoutParams.height != view.measuredHeight) {
viewPager.layoutParams = (viewPager.layoutParams).also { lp -> lp.height = view.measuredHeight }
}
}
}
})

Real slow pagination with NestedScrollView

In my application, I am using RecyclerView inside NestedScrollView and also, I am implementing pagination with this. I am attaching my piece of code below for implementing nested scroll view with pagination.
I have a large no data attaching to recyclerview, therefore, this data takes a lot of time attaching which effects the smooth scrolling of the list.
I had only a recylerview which had simple scrolling issues, wasn't smooth, but still pagination was working fine. I inserted my recyclerview inside the nestedscrollview, the scrolling becomes smooth only for the first page. But when pagination is called, the scrolling gets effected very badly.
When the page number increments, the scrolling goes from bad to worst.
Let me show my code that I used for nested view scrolling and pagination.
home.xml:
<android.support.v4.widget.NestedScrollView
android:id="#+id/nestedScroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none" />
</android.support.v4.widget.NestedScrollView>
And this is my Fragment Class.java:
#BindView(R.id.nestedScroll)
NestedScrollView nestedScroll;
private int currentPage = 0;
nestedScroll.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
View view = (View) nestedScroll.getChildAt(nestedScroll.getChildCount() - 1);
int diff = (view.getBottom() - (nestedScroll.getHeight() + nestedScroll.getScrollY()));
if (diff == 0) {
if (blnCheckREsult == false) {
blnCheckREsult = true;
if (ConnectivityReceiver.isConnected()) {
currentPage++;
getHomeData(currentPage);
} else {
Toast.makeText(activity, "Please check your internet connection", Toast.LENGTH_SHORT).show();
}
}
}
}
});
I want to implement smooth scrolling even when the adapter takes time while binding data to the view holder. But the scrolling becomes really slow when setting data inside the adapter.
Or If I am missing something else, please correct me.
Thanks in advance.
Try set this in your RecyclerView:
rv.setNestedScrollingEnabled(false);
before set adapter.

Android ImageButton swap resource leaves 'residue'

I have 2 Images:
IMG_1:
IMG_1_PRESSED:
I display IMG_1 by default, but when I click on it, the image should change to IMG_1_PRESSED.
Here's a code snippet of the button itself, where be is an enum containing the correct drawables:
ImageButton ib = new ImageButton(this.context);
ib.setImageResource(be.getDrawable());
ib.setScaleType(ImageButton.ScaleType.CENTER_INSIDE);
ib.setBackgroundColor(Color.TRANSPARENT);
ib.setPadding(0, 0, 0, 0);
ib.setAdjustViewBounds(true);
ib.setMaxWidth(width);
ib.setMaxHeight(height);
setStrengthListener(ib, be);
And here is the setStrengthListener method:
private void setStrengthListener(final ImageButton ib, final ButtonEnum be){
ib.setOnTouchListener(new View.OnTouchListener() {
#SuppressLint("ClickableViewAccessibility")
#Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
ib.setImageResource(be.getDrawablePressed());
return true;
case MotionEvent.ACTION_UP:
ib.setImageResource(be.getDrawable());
return true;
}
return false;
}
});
}
Now both images are 480x240 and width and height are set to device's width/3 and device's width/6 respectively (I want to display 3 images, hence the /3).
Here's the problem, if the button is pressed, I get this:
There's this little line under the pressed button... I've tried a lot to fix this but somehow that little line has made itself my greatest enemy today.
As an extra effort: the line doesn't show anymore if I set the original resource to IMG_1_PRESSED:
ImageButton ib = new ImageButton(this.context);
ib.setImageResource(be.getDrawablePressed());
ib.setScaleType(ImageButton.ScaleType.CENTER_INSIDE);
ib.setBackgroundColor(Color.TRANSPARENT);
ib.setPadding(0, 0, 0, 0);
....etc
But obviously I don't want to start with a pressed button.
So dear Android experts, what am I doing wrong here?
This is the simplest solution as i know,
Try this
create Xml inside your Drawable folder
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="#drawable/btn_pressed_img"android:state_pressed="true" />
<item android:drawable="#drawable/btn_normal_image" />
</selector>
Then add this drawable file as background to your View
Example :
<ImageView
android:id="#+id/enter_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxHeight="100dip"
android:maxWidth="100dip"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:background="#drawable/enter_btn" />
or in Java code
yourView.setImageResource(R.drawable.*drawablefile*);
You can use ImageButton also but ImageView would look better than ImageButton since it doesn't have button borders.
So after a hell of a lot of trial and error, I managed to get that grey line away. In the end it was very simple really, though I'm not completely sure why this fixed it:
The main layout:
LinearLayout ll = new LinearLayout(this);
ll.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
ll.setOrientation(LinearLayout.HORIZONTAL);
ll.setGravity(Gravity.TOP);
The view creation (layout for imageview is unchanged):
ImageView iv = tbb.getTopBarButton(ButtonEnum.IMG_1);
LinearLayout iViewLayout = new LinearLayout(this);
iViewLayout.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
iViewLayout.setOrientation(LinearLayout.HORIZONTAL);
iViewLayout.addView(iv);
ll.addView(iViewLayout);
Adding a separate layout per imageview seemed to remove the grey line. Would love to know WHY this works though, but for future reference, at least I hope to help others.

Transfer scroll event from sliding view to ScrollView - Sliding panel with ScrollView like Google Maps

So I'm using the Sliding Up Panel Library in my application, and I'm trying to implement a ScrollView inside the sliding panel. Since both the sliding panel and the ScrollView are controlled by vertical scrolls, this is causing me some issues.
I've partially got it to work by switching the panel's dragview once the panel has been slid all the way up, and when the ScrollView has been scrolled to the top.
The problem I'm facing now is that, when scrolling the panel to top the scrolling doesn't transfer to the ScrollView, like it does in Google Maps. Little hard to explain, so look at the video here:
www.youtube.com/watch?v=9MUsmQzusX8&feature=youtu.be
This is the panel slide listener:
...
slidePanel.setEnableDragViewTouchEvents(true);
slidePanel.setPanelSlideListener(new SlidingUpPanelLayout.PanelSlideListener() {
#Override
public void onPanelSlide(View panel, float slideOffset) {
// Change the dragview to panelheader when panel is fully expanded
// I'm doing this here instead of in onPanelExpanded,
// because onPanelExpanded first gets called once scroll
// is released.
if (slideOffset <= 0) {
slidePanel.setDragView(layoutPanelTop);
}
// If the panel is not fully expanded set the whole
// panel as dragview
else if(slideOffset > 0) {
slidePanel.setDragView(layoutPanel);
}
}
}
#Override
public void onPanelExpanded(View panel) {
// layout.setDragView(layoutPanelTop);
panelCollapsed = false;
panelExpanded = true;
panelAnchored = false;
Log.v("TAG, "panelExpanded");
}
#Override
public void onPanelCollapsed(View panel) {
slidePanel.setDragView(layoutPanel);
panelCollapsed = true;
panelExpanded = false;
panelAnchored = false;
Log.v(TAG, "panelCollapsed");
}
#Override
public void onPanelAnchored(View panel) {
slidePanel.setDragView(layoutPanel);
panelCollapsed = false;
panelExpanded = false;
panelAnchored = true;
Log.v(TAG, "panelAnchored");
}
});
And I have managed to create a fully working scrollview listener by extending scrollview, which can detect scroll direction and onDown and onUp motion events:
private boolean atScrollViewTop = false;
#Override
public void onScrollChanged(int scrollY) {
scrollY = Math.min(mMaxScrollY, scrollY);
if (scrollY <= 0) {
Log.v("myTag", "You at scrollview top");
atScrollViewTop = true;
} else {
atScrollViewTop = false;
}
mScrollSettleHandler.onScroll(scrollY);
switch (mState) {
case STATE_SCROLL_UP:
if (panelExpanded && atScrollViewTop) {
slidePanel.setDragView(layoutPanel);
} else {
slidePanel.setDragView(layoutPanelTop);
}
Log.v("myTag", "scrolling up");
break;
case STATE_SCROLL_DOWN:
slidePanel.setDragView(layoutPanelTop);
Log.v("myTag", "scrolling down");
break;
}
}
#Override
public void onDownMotionEvent() {
}
#Override
public void onUpOrCancelMotionEvent() {
}
I've been struggling with this the last two days.. So really hope on some pointer at least. Thanks very much in advance. Regards Jakob Harteg.
Sorry for delay.. i find the solution.
image = (ImageView) findViewById(R.id.image); //Layout to slide
SlidingUpPanelLayout layout = (SlidingUpPanelLayout)
findViewById(R.id.sliding_layout);
layout.setDragView(image);
/*This method sets the layout to be used only
in the sliding panel, while all layouts children
are able to perform other functions such as scrolling */
And this is the layout
<..SlidingUpPanelLayout
android:id="#+id/sliding_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"
android:gravity="center" />
<LinearLayout
android:id="#+id/slide_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="#+id/image"
android:layout_width="match_parent"
android:layout_height="35dp"
android:background="#drawable/ec_image"/>
<!-- FINALLY SCROLLVIEW -->
<ScrollView .... />
Hope it is useful.
I'm guessing ScrollView is child of the SlidingPanel?
In that case, override onInterceptTouchEvent to your SlidingPanel to intercept the onTouch event of your ScrollView when y = 0.
onInterceptTouchEvent does the following two:
child gets action cancel event
parent get the event trough onTouch
I don't know if I've arrived to late but after working hard some days I've found that AndroidSlidingUp panel has a method called setScrollView who handles scroll events properly.
I hope that this post will be useful because I was spending much time searching and I didn't find some tip that help me.

How do I clear ListView selection?

TL;DR: You choose an option from (a) my listview. Then, you change your mind and type something in (b) my edit text. How do I clear your listview selection and only show your edittext? (and vice versa)
I have an application with a listview of options as well as an edittext to create an own option. I need the user to either choose or create an option, but not both. Here's a drawing of my layout:
Whenever the user selects an option from the listview, I set it as "selected" by making it green, like so:
<?xml version="1.0" encoding="utf-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_selected="true"
android:drawable="#color/colorPrimary"/>
<item
android:state_selected="false"
android:drawable="#color/windowBackground" />
</selector>
(this is set as the background of my listview)
Problem: I want to unselect the listview option if the user decides to type in their own option since they can only have one option.
User selects an option from the listview
User decides they want to create their own option using the edittext
The listview option is unselected when they start typing their own
I've tried doing the following, but nothing unselects.
e.setOnEditorActionListener(new TextView.OnEditorActionListener()
{
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
for(int i=0; i<=5; i++){
listView.setItemChecked(i, false);
}
listView.clearChoices();
listView.requestLayout()
adapter.notifyDataSetChanged()
}
}
A very puzzling predicament, any help is appreciated!
Edit: here is the layout of the edittext:
<EditText
android:id="#+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBaseline="#+id/textView4"
android:layout_alignBottom="#+id/textView4"
android:layout_toEndOf="#+id/textView4"
android:layout_toRightOf="#+id/textView4"
android:color="#color/colorPrimary"
android:imeOptions="actionDone"
android:inputType="text"
android:textColor="#color/textColorPrimary"
android:textColorHint="#color/colorPrimary" />
Edit: here is the layout of the listview:
<ListView
android:id="#+id/listview"
android:layout_width="wrap_content"
android:layout_height="250dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/toolbar"
android:background="#drawable/bg_key"
android:choiceMode="singleChoice"
android:listSelector="#color/colorPrimary">
</ListView>
Long Story Short
ListView selector (android:listSelector) is designed to indicate a click event, but not selected items.
If a ListView selector is drawn (after first click) it won't dissapear without drastic changes in the ListView
Hence use only drawables with transparent background if no state is applied to it as a ListView selector. Don't use a plain color resource for it, don't confuse yourself.
Use ListView choice mode (android:choiceMode) to indicate selected items.
ListView tells which row is selected by setting android:state_activated on the row's root view. Provide your adapter with corresponding layout/views to represent selected items correctly.
TL/DR Solutions
You can hide/remove selector with one of the following:
Making the selector transparent getSelector().setAlpha(0)
Resetting the current adapter with setAdapter(myAdapter) (adapter might be the same)
Solutions that might or might not work, depending on the OS version:
Making the list view to refresh layout completely via requestLayout(), invalidate() or forceLayout() methods;
Making the list view to refresh layout via notifyDataSetChanged()
Theory
Well, the built-in selection in ListView is utterly tricky at a first glance. However there are two main distinctions you should keep in mind to avoid confusing like this - list view selector and choice mode.
ListView selector
ListView selector is a drawable resource that is assumed to indicate an event of clicking a list item. You can specify it either by XML-property android:listSelector or using method setSelector(). I couldn't find it in docs, but my understanding is that this resource should not be a plain color, because after it's being drawn, it won't vanish without drastic changes in the view (like setting an adapter, that in turn may cause some glitches to appear), hence such drawable should be visible only while particular state (e.g. android:state_pressed) is applied. Here is a simple example of the drawable that can be used as a List View selector
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="#android:color/darker_gray" />
<item
android:drawable="#android:color/transparent" />
</selector>
For whatever reason you cannot use a Color State List as List View selector, but still can use plain colors (that are mostly inappropriate) and State List drawables. It makes things somewhat confusing.
After the first click on a List View happens, you will not be able to remove List View selector from the List View easily.
The main idea here is that List View selector is not designed to indicate selected item.
ListView choice mode
ListView choice mode is assumed to indicate selected items. As you might know, primarily there are two choice modes we can use in ListView - Single Choice and Multiple Choice. They allow to track a single or multiple rows selected respectively. You can set them via android:choiceMode XML-property or setChoiceMode() method.
The ListView itself keeps selected rows in it and let them know which one is selected at any given moment by setting android:state_activated property of the row root view. In order to make your rows reflect this state, their root view must have a corresponding drawable set, e.g. as a background. Here is an example of such drawable:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_activated="true"
android:drawable="#android:color/holo_green_light" />
<item
android:drawable="#android:color/transparent" />
</selector>
You can make rows selected/deselected programmatically using the setItemChecked() method. If you want a ListView to clear all selected items, you can use the clearChoices() method. You also can check selected items using the family of the methods: getCheckedItemCount(), getCheckedItemIds(), getCheckedItemPosition() (for single choice mode), getCheckedItemPositions() (for multiple choice mode)
Conclusion
If you want to keep things simple, do not use the List View selector to indicate selected items.
Solving the issue
Option 1. Dirty fix - hide selector
Instead of actually removing selector, changing layouts and implementing a robust approach, we can hide the selector drawable when it's needed and show it later when clicking a ListView item:
public void hideListViewSelector() {
mListView.getSelector().setAlpha(0);
}
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (mListView.getSelector().getAlpha() == 0) {
mListView.getSelector().setAlpha(255);
}
}
Option 2. Thoughtful way
Let's go through your code and make it comply the rules i described step by step.
Fix ListView layout
In your ListView layout the selector is set to a plain color, and therefore your items are colored by it when they are clicked. The drawable you use as the ListView background have no impact, because ListView state doesn't change when its rows are clicked, hence your ListView always has just #color/windowBackground background.
To solve your problem you need at first remove the selector from the ListView layout:
<ListView
android:id="#+id/listview"
android:layout_width="wrap_content"
android:layout_height="250dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/toolbar"
android:listSelector="#color/colorPrimary"
android:background="#color/windowBackground"
android:choiceMode="singleChoice"/>
Make your rows reflect activated state
In the comments you give your adapter as follows:
final ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, text1, listOfThings);
You also asked me if it's possible to keep using standard adapter to achieve desired behavior. We can for sure, but anyway a few changes are required. I can see 3 options for this case:
1. Using standard android checked layout
You can just specify a corresponding standard layout - either any of the layouts that use CheckedTextView without changed background drawable as the root component or of those that use activatedBackgroundIndicator as their background drawable. For your case the most appropriate option should be the simple_list_item_activated_1. Just set it as in your ArrayAdapter constructor like this:
final ArrayAdapter<String> adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_activated_1, android.R.id.text1, listOfThings);
This option is the closest to what i understand by 'standard' adapter.
2. Customize your adapter
You can use standard layout and mostly standard adapter with a small exception of getting a view for your items. Just introduce an anonymous class and override the method getView(), providing row views with corresponding background drawable:
final ArrayAdapter<String> adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1, listOfThings) {
#NonNull
#Override
public View getView(int position, #Nullable View convertView, #NonNull ViewGroup parent) {
final View view = super.getView(position, convertView, parent);
if (convertView == null) {
view.setBackgroundResource(R.drawable.list_item_bg);
}
return view;
}
};
3. Customize your layout
The most common way of addressing this issue is of course introducing your own layout for the items view. Here is my simple example:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="#drawable/list_item_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="16dp">
<TextView
android:id="#android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>
I saved it in a file /res/layout/list_view_item.xml Do not forget setting this layout in your adapter:
final ArrayAdapter<String> adapter = new ArrayAdapter(this, R.layout.list_view_item, android.R.id.text1, listOfThings);
Clearing selection
After that your rows will reflect selected state when they are clicked, and you can easily clear the selected state of your ListView by calling clearChoices() and consequence requestLayout() to ask the ListView to redraw itself.
One little comment here that if you want unselect the item when user start typing, but not when he actually clicks the return (done) button, you need to use a TextWatcher callback instead:
mEditText.addTextChangedListener(new TextWatcher(){
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (mListView.getCheckedItemCount() > 0) {
mListView.clearChoices();
mListView.requestLayout();
}
}
#Override
public void afterTextChanged(Editable s) {}
});
Hopefully, it helped.
I have a good solution to do that. Add EditText to your layout which contains on your ListView as this layout:
<RelativeLayout
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"
android:layout_width="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:id="#+id/list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:choiceMode="singleChoice"
/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Comment"
android:layout_below="#id/list_view"
android:id="#+id/editText"
android:nextFocusUp="#id/editText"
android:nextFocusLeft="#id/editText"/>
</RelativeLayout>
Then initialize Boolean variable to check whether editText if focused or not for example use this : boolean canBeSelected = true;
Then after setting adapter use this code:
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
if (canBeSelected) {
listView.setSelector(R.drawable.background);
listView.setSelected(true);
listView.setSelection(i);
} else {
if (!editText.isFocused()){
canBeSelected = true;
listView.setSelector(R.drawable.background);
listView.setSelected(true);
listView.setSelection(i);
}
}
}
});
editText.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
canBeSelected = false;
Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
listView.setSelector(transparentDrawable);
listView.clearChoices();
listView.setSelected(false);
return false;
}
});
editText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
if (editText.isFocused()){
Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
listView.setSelector(transparentDrawable);
listView.clearChoices();
listView.setSelected(false);
canBeSelected = false;
}
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
listView.setSelector(transparentDrawable);
listView.clearChoices();
listView.setSelected(false);
canBeSelected = false;
}
#Override
public void afterTextChanged(Editable editable) {
if (editText.isFocused()) {
Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT);
listView.setSelector(transparentDrawable);
listView.clearChoices();
listView.setSelected(false);
canBeSelected = false;
}
}
});
}
Hope it works with you :)
Re-setting the adapter in the edittext listener worked for me:
editText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
listview.clearChoices();
listview.setAdapter(adapter);
Toast.makeText(getApplicationContext(),"Typing" + listview.getSelectedItemPosition(), Toast.LENGTH_SHORT).show();
}
#Override
public void afterTextChanged(Editable editable) {
}
});
I put the selected index in a toast to check if the item was correctly deselected.
Hope this works!!
Just call clear when you make the request for the second data set:
arrayAdapter!!.clear()
You load your first data set
The user select one elements,
This action highlight your item
For any reason you launch the reload of your data set (because edittext's value changed),
at this moment call, clear() on your adapter.
Then you retrieved your dataset, you send it to the arrayAdapter and
No one is selected .
This is because when you clear, it also clear the selected flag

Categories

Resources