Shared element transition get weird behavior while something is scrolling - android

I'm using shared element transition between activities. The first activity contains a LinearLayout and a RecyclerView:
The second activity:
And here's my code, seems very simple:
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) getContext(), pairs);
ActivityCompat.startActivity(getContext(), new Intent(Intent.ACTION_VIEW, scheme), options.toBundle());
In common cases, everything seems ok, the transition animation works perfectly. But if I start the next activity while the RecyclerView in the first activity is scrolling, then go back to the first activity, something terrible happens:
I also tested ListView and ViewPager, ListView has same problem while ViewPager not. If I stop the scroll before startActivity, everything will be fine.
However in my product environment, views are complicated and it is ugly to find all ListViews and RecyclerViews and stop their scrolls. Is there anyway to prevent it from happening other than stop scrolling before startActivity?

RecyclerView class don't have such method to disable scrolling. Create CustomRecyclerView yourself like one below.
public class FooRecyclerView extends RecyclerView {
private boolean verticleScrollingEnabled = true;
public void enableVersticleScroll (boolean enabled) {
verticleScrollingEnabled = enabled;
}
public boolean isVerticleScrollingEnabled() {
return verticleScrollingEnabled;
}
#Override
public int computeVerticalScrollRange() {
if (isVerticleScrollingEnabled())
return super.computeVerticalScrollRange();
return 0;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent e) {
if(isVerticleScrollingEnabled())
return super.onInterceptTouchEvent(e);
return false;
}
public FooRecyclerView(Context context) {
super(context);
}
public FooRecyclerView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public FooRecyclerView(Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
}
replace RecyclerView to above created customview,
<com.customGoogleViews.FooRecyclerView
android:id="#+id/task_sub_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
disable scrolling before starting new activity,
recyclerview.enableVersticleScroll(false);
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) getContext(), pairs);
ActivityCompat.startActivity(getContext(), new Intent(Intent.ACTION_VIEW, scheme), options.toBundle());
Hope it helps :)

Related

Android RecyclerView plus ViewPager

I have a ViewPager that utilizes a RecyclerView for each page and shares ViewItem rows across pages. Accordingly I share a single RecyclerViewPool between them. However, the ViewPager loads each RecyclerView whether or not it is the page on screen. Is there a way to indicate to the RecyclerView that all of its items are offscreen and force its views to be returned to the Recycler?
My sense is that subclassing LinearLayoutManager and overriding its onLayoutChildren method is the way to go, but I don't have much experience with LayoutManager and would like some guidance.
So here is a subclass of LinearLayoutManager that operates the way I described:
public class PageVisibleLinearLayoutManager extends LinearLayoutManager {
public PageVisibleLinearLayoutManager(Context context) {
super(context);
}
public PageVisibleLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public PageVisibleLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private boolean pageVisible = true;
void setPageVisible(boolean pageVisible) {
boolean change = (this.pageVisible != pageVisible);
this.pageVisible = pageVisible;
if(change) requestLayout();
}
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if(pageVisible) {
super.onLayoutChildren(recycler, state);
} else {
removeAndRecycleAllViews(recycler);
}
}
}
It works nicely and gives up its views if requested. As dsh mentioned, it is important to mark adjacent pages as being on screen (and I really don't know why setOffscreenPageLimit doesn't limit the number of pages loaded as expected). My previous solution was to use ViewStub and inflate a page only when it was on screen or adjacent. The layout manager method is slightly faster upon initial turning to an unloaded page, but ViewStub has the advantage of pages staying in memory once loaded (making subsequent scrolling more smooth), so I decided to stick with that.
Thank you all. Next question...

Using onInterceptTouchEvent in a class that extends Fragment

I have Fragment with a View which contains some clickable elements (ToggleButtons and a GridView).
I have set an onTouchListener on the View which I use to detect simple swipe gestures, this works perfectly well as long as the swipe doesn't start on any of the clickable items. I would like the swipe to work regardless of where it starts.
I understand that the ToggleButtons and GridView are probably consuming the touch event and in classes which extend ViewGroup I could override onInterceptTouchEvent or dispatchTouchEvent in subclasses of Activity.
Any ideas how I could deal with this situation when extending Fragment?
The closest I've found here is: Android adding onInterceptTouchEvent to a fragment Unfortunately there have been no replies to this question.
Thanks in anticipation.
Okay, got there in the end.
I create a subclass of RelativeLayout which is the root of my layout and then within this I override onInterceptTouchEvent.
public class RootLayout extends RelativeLayout {
public RootLayout(Context context) {
super(context);
}
public RootLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RootLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev){
return true;
}
}
Then replace the RelativeLayout in my xml with the new subclass.
<com.walker.leigh.dipswitch2.RootLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
Thanks for your help :)

Scrollup not working in StickyListviewHeader with SwipeRefreshLayout

I am using StickyHeaderListview in my project to display contents and for refreshing the list, I am using SwipeRefreshLayout.
The problem here is, when I try to scroll up the list, it starts refreshing the list and not allowing to view the previous items of list.
I want the behavior should be such as the list get refresh only when I've reached to the first item and I try to scroll up , not everytime when i scroll up the list.
Can anyone help on this?
P.s. For implementing SwipeRefreshLayout, I am refering this example
I faced the same problem when using StickyHeaderListview as a direct child of SwipeRefreshLayout. StickyHeaderListview is in fact a FrameLayout wrapping a ListView inside. As nitesh goel explained, this would lead to problems with canChildScrollUp(). Based on nitesh goel's example, this is a full version of CustomSwipeRefreshLayout that works well for me:
public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {
/**
* A StickyListHeadersListView whose parent view is this SwipeRefreshLayout
*/
private StickyListHeadersListView mStickyListHeadersListView;
public CustomSwipeRefreshLayout(Context context) {
super(context);
}
public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setStickyListHeadersListView(StickyListHeadersListView stickyListHeadersListView) {
mStickyListHeadersListView = stickyListHeadersListView;
}
#Override
public boolean canChildScrollUp() {
if (mStickyListHeadersListView != null) {
// In order to scroll a StickyListHeadersListView up:
// Firstly, the wrapped ListView must have at least one item
return (mStickyListHeadersListView.getListChildCount() > 0) &&
// And then, the first visible item must not be the first item
((mStickyListHeadersListView.getFirstVisiblePosition() > 0) ||
// If the first visible item is the first item,
// (we've reached the first item)
// make sure that its top must not cross over the padding top of the wrapped ListView
(mStickyListHeadersListView.getListChildAt(0).getTop() < 0));
// If the wrapped ListView is empty or,
// the first item is located below the padding top of the wrapped ListView,
// we can allow performing refreshing now
} else {
// Fall back to default implementation
return super.canChildScrollUp();
}
}
}
Ok I have got it working. If the SwipeRefreshLayout is the root of the layout and the ListView resides deep into the hierarchy (I had put the ListView inside a RelativeLayout along with the empty TextView) and not the direct child of the SwipeRefreshLayout, it won’t detect a swipe up on the list view properly.
You should create a custom class that extends SwipeRefreshLayout and override canChildScrollUp() method in SwipRefreshLayout
Here is a example :
public class CustomSwipeRefreshLayout extends SwipeRefreshLayout{
private AbsListView view;
public CustomSwipeRefreshLayout(Context context) {
super(context);
}
public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setView(AbsListView view){
this.view=view;
}
#Override
public boolean canChildScrollUp() {
return view.getFirstVisiblePosition()!=0;
}
}
I have had a similar problem, the direct child should be an instance of ScrollView (or ListView). The SwipeRefreshLayout will only take in account the direct child's scroll and not the child's of that direct child. I managed to solve this by using two SwipeRefreshLayouts.
I posted the code on github.
Hi i think i made something for a generally use :
public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {
private View v;
public CustomSwipeRefreshLayout(Context context) {
super(context);
}
public CustomSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setView(View v) {
this.v = v;
}
#Override
public boolean canChildScrollUp() {
return this.v.canScrollVertically(-1);
}
}
With that solution, only set the view you want to scroll inside the SwipeRefreshLayout, after call canChildScrollUp(). like this :
this.refreshLayout.setView(aView);
this.refreshLayout.canChildScrollUp();
I don't test it a lot, but if i'm right it will work for every view at every place (direct child or not) in the SwipeRefreshLayout.
(for me it was SwipeRefreshLayout => RelativeLayout => SrcollView => linearLayout)
This is very simple solution:
list.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
int topRowVerticalPosition = (list == null || list.getChildCount() == 0) ?
0 : list.getChildAt(0).getTop();
swipeRefreshLayout.setEnabled((topRowVerticalPosition >= 0));
}
});
So, if you're on the top of the listview you will be enabled to do refresh.

Combining PullToRefreshListView and StickyHeaderListView, list view doesn't remember scroll position on fragment change

I need a ListView that combines the functionality of a PullToRefreshListView and a StickyHeaderListView by creating a class called PullToRefreshStickyListView which is an extension of PullToRefreshBase the class is here:
public class PullToRefreshStickyListView extends PullToRefreshBase<StickyListHeadersListView>{
public PullToRefreshStickyListView(Context context) {
super(context);
}
public PullToRefreshStickyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public com.handmark.pulltorefresh.library.PullToRefreshBase.Orientation getPullToRefreshScrollDirection() {
return Orientation.VERTICAL;
}
#Override
protected StickyListHeadersListView createRefreshableView(Context context, AttributeSet attrs) {
StickyListHeadersListView view = new StickyListHeadersListView(context, attrs);
return view;
}
#Override
protected boolean isReadyForPullEnd() {
return false;
}
#Override
protected boolean isReadyForPullStart() {
StickyListHeadersListView view = getRefreshableView();
if (view.getWrappedList().getChildCount() == 0)
return true;
return view.getWrappedList().getChildAt(0).getTop() == 0;
}
}
All is fine, but my problem is, when the fragment that contains this class is shown for the first time, hidden, and shown again for a second time, the list view doesn't seem to remember its last vertical scroll position. Am I missing something? I'm thinking I should be doing something with onSaveInstance, but I am not overly familiar with this so I don't really know why. Maybe someone can give me pointers? Thanks.
Nevermind, I've discovered how to fix this. Turns out that a list view needs to have an ID in order for its scroll state to be remembered. I've just added this line in this method:
#Override
protected StickyListHeadersListView createRefreshableView(Context context, AttributeSet attrs) {
StickyListHeadersListView view = new StickyListHeadersListView(context, attrs);
view.setId(R.id.sticky_header);
return view;
}
created the file ids.xml in res, and added an id for the sticky header view like so:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="sticky_header"></item>
</resources>
and that certainly did the trick.
Remember kids, give your ListViews their own id's.

Disable All Touch Screen Interactions While Animation

I wish to disable all the touch screen interactions while an animation is being displayed.
I don't wish to use the setClickable() method on the buttons at the start or end of the animation because there are a large number of buttons. Any suggestions?
In your Activity, you can override onTouchEvent and always return true; to indicate you are handling the touch events.
You can find the documentation for that function there.
Edit Here is one way you can disable touch over the whole screen instead of handling every view one by one... First change your current layout like this:
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
< .... put your current layout here ... />
<TouchBlackHoleView
android:id="#+id/black_hole"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</FrameLayout>
And then define your custom view with something like this:
public class TouchBlackHoleView extends View {
private boolean touch_disabled=true;
#Override
public boolean onTouchEvent(MotionEvent e) {
return touch_disabled;
}
public disable_touch(boolean b) {
touch_disabled=b;
}
}
Then, in the activity, you can disable the touch with
(TouchBlackHoleView) black_hole = findViewById(R.id.black_hole);
black_hole.disable_touch(true);
And enable it back with
black_hole.disable_touch(false);
Easy way to implement that is add transaperent layout over it (add it in your xml fill parent height and width).
In the animation start: transaparentlayout.setClickable(true);
In the animation end: transaparentlayout.setClickable(false);
answer to this issue
for (int i = 1; i < layout.getChildCount(); i++) {
TableRow row = (TableRow) layout.getChildAt(i);
row.setClickable(false);
selected all the rows of the table layout which had all the views and disabled them
Eventually I took as a basic answer of #Matthieu and make it work such way. I decide to publish my answer because it take me maybe 30 min to understood why I got error.
XML
<...your path to this view and in the end --> .TouchBlackHoleView
android:id="#+id/blackHole"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Class
public class TouchBlackHoleView extends View {
private boolean touchDisable = false;
public TouchBlackHoleView(Context context) {
super(context);
}
public TouchBlackHoleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TouchBlackHoleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
return touchDisable;
}
public void disableTouch(boolean value){
touchDisable = value;
}
}
Using
blackHole = (TouchBlackHoleView) findViewById(R.id.blackHole);
blackHole.disableTouch(true);
Enjoy

Categories

Resources