I have a fragment that uses the new CoordinatorLayout/AppBarLayout/CollapsingToolbarLayout paradigm, and I'd like to be able to detect when the collapsing toolbar is fully expanded so that I can perform an operation on the entire fragment it's in, e.g. popping the fragment off the stack and going to a new one, dismissing the fragment. I have the dismissing code working, I just need to know when and when not to use it.
I've experimented a bit with AppBarLayout.OnOffsetChangedListener, but didn't have much luck. Is there a way to use it to determine when things are completely expanded, or is there a more preferred method someone knows about?
Thanks in advance!
EDIT:
I also see there are a couple implementations for AppBarLayout.setExpanded(...), however not AppBarLayout.getExpanded() or something similar, so I'm stumped there too.
It doesn't look like there's anything in the APIs, but the following seems to be working for me. It might need testing.
boolean fullyExpanded =
(appBarLayout.getHeight() - appBarLayout.getBottom()) == 0;
Edit: The above solution does seem to work, but since I wanted to test this condition when the appbar was scrolled, I ended up using the following solution with OnOffsetChangedListener.
class Frag extends Fragment implements AppBarLayout.OnOffsetChangedListener {
private boolean appBarIsExpanded = true;
private AppBarLayout appBarLayout;
#Override
public void onActivityCreated(Bundle state) {
super.onActivityCreated(state);
appBarLayout = (AppBarLayout) getActivity().findViewById(R.id.app_bar);
}
#Override
public void onResume() {
super.onResume();
appBarLayout.addOnOffsetChangedListener(this);
}
#Override
public void onStop() {
super.onStop();
appBarLayout.removeOnOffsetChangedListener(this);
}
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
appBarIsExpanded = (verticalOffset == 0);
}
}
My solution is based on creating a custom view. First create a class extending the native AppBarLayout:
public class CustomAppBar extends AppBarLayout { ....
Then inside the class set an addOnOffsetChangedListener like this:
this.addOnOffsetChangedListener...
You can do the above by setting in the constructor or maybe by calling a method inside the constructor. So you need the constructor, remember to use the constructor with 2 params to be able to added to the xml:
public CustomAppBar(Context context, AttributeSet attrs) {
super(context, attrs);
//You can set the listener here or maybe call the method that set the listener
}
Then we have to get access to the state of the view, so create a private boolean inside your custom view class, and set it to true or false if your view start expanded or collapsed, in this case my view is by default expanded:
private boolean isExpanded = true;
Now you have to update the state of that boolean:
this.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (verticalOffset == 0) {
isExpanded = true;
} else {
isExpanded = false;
}
}
});
Next step is to get the state of the boolean by using a getter inside the CustomAppBar class
public boolean isExpanded() {
return isExpanded;
}
The next is go to your xml, use your custom view there, then in the Acivity or Fragment get the view and use the method to know the AppBar status
I know, that it maybe a bit late, but exploring the source code of the AppBArLayout I have found, that the AppBarLayout.OnOffsetChangedListener just translates the value of the int getTopAndBottomOffset() of the AppBar's Behaviour.
So at any time you can just use this code to define whether an AppBarLayout expanded or not:
public boolean isAppBArExpanded(AppBarLayout abl) {
final CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) abl.getLayoutParams()).getBehavior();
return (behavior instanceof AppBarLayout.Behavior) ? (((AppBarLayout.Behavior) behavior).getTopAndBottomOffset() == 0) : false;
}
Related
I have a recyclerview with gridlayoutmanager.
If I run the code
recycler.smoothScrollTo(adapter.getItemCount())
the recycler scrolls really fast to the last element. I tried some solutions on Stackoverflow to make the scrolling slower, but all apply to Linearlayoutmanager not Gridlayoutmanager.
Any help?
I cannot say for sure what your problem is. But I am lucky enough to have a very simple GridLayoutManager recyclerview demo out there, very small sample project. I created a so branch and added a button that does the same you do.
Look it up: https://github.com/Gryzor/GridToShowAds/compare/so?expand=1
.setOnClickListener { mainRecyclerView.smoothScrollToPosition(data.size) }
And that alone just works.
Check the source code, it's a very simple sample for something unrelated, but happens to have a RV with a Grid Layout :)
UPDATE
What you actual want is to control the Speed at which the recyclerView scrolls. Ok.
It's not the RecyclerView that drives the scroll, it's actually the LayoutManager that does. How so?
If you look at RV's source code...
public void smoothScrollToPosition(int position) {
...
mLayout.smoothScrollToPosition(this, mState, position);
}
So it ends up calling mLayout. What is this?
#VisibleForTesting LayoutManager mLayout;
So, your LayoutManager#smoothScroll... method is used.
Decompiling now GridLayoutManager for science:
#Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext());
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
note: this method is actually in LinearLayoutManager because GridLayoutManager is a subclass and it doesn't override the method
A LinearSmoothScroller!; no parameter to specify the speed though...
Look at it:
public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
private static final boolean DEBUG = false;
private static final float MILLISECONDS_PER_INCH = 25f;
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
...
}
This class has a start() method described as:
* Starts a smooth scroll for the given target position.
So who calls this?
The mLayout.smoothScrollToPosition method does at the end in the startSmoothScroll(...) call.
public void startSmoothScroll(SmoothScroller smoothScroller) {
Starts a smooth scroll using the provided {#link SmoothScroller}.
mSmoothScroller.start(mRecyclerView, this);
So... in lieu of all this, the answer to your question is:
You need to create your extension of GridLayoutManager by subclassing it, and in it, override the smoothScrollToPosition method, to provide your own Scroller logic.
Thread carefully though, LayoutManagers are not the "simplest" classes of all time and they can be quite complicated to master.
Good luck! :)
My simple working solution currently is still implementing a timer then working with it.
final CountDownTimer scrollUp_timer = new CountDownTimer(50000, 30) {
#Override
public void onTick(long millisUntilFinished) {
if (layoutManager != null && layoutManager.findFirstVisibleItemPosition() != 0) searchRecyclerView.smoothScrollToPosition(layoutManager.findFirstVisibleItemPosition()-1);
}
#Override
public void onFinish() {
try{
}catch(Exception e){
// log
}
}
};
scrollUp.setOnDragListener(new View.OnDragListener() {
#Override
public boolean onDrag(View view, DragEvent dragEvent) {
layoutManager = ((GridLayoutManager)searchRecyclerView.getLayoutManager());
int action = dragEvent.getAction();
if (action == DragEvent.ACTION_DRAG_ENTERED) {
scrollUp_timer.start();
} else if (action == DragEvent.ACTION_DRAG_EXITED) {
searchRecyclerView.scrollBy(0,0);
scrollUp_timer.cancel();
}
return true;
}
});
You can extend:
class CSCustomRecyclerSmoothScroller(context: Context, speed: Float = 0.2f)
: LinearSmoothScroller(context) {
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float = speed
}
And use it like:
val shortAnimationDuration =
view.resources.getInteger(android.R.integer.config_shortAnimTime)
val scroller = CSCustomRecyclerSmoothScroller(this, speed = 0.15)
scroller.targetPosition = position
view.postDelayed({
layoutManager.startSmoothScroll(scroller)
}, shortAnimationDuration.toLong())
postDelayed can be necessary in some cases but maybe not in all.
I use similar code with GridLayoutManager I just tried to extract relevant parts from my way of writing things.
I have a Snackbar which is as follows:
However, if the drop down of the AutoCompleteTextView is too long, the drop down will block the Snackbar.
As you can see in the above image, the Snackbar is actually showing. However, its visibility is blocked by the long drop down. You can see from the above image
I try to use the following Snackbar code. Adding bringToFront() doesn't help much.
private void showSnackbar(String message) {
Snackbar snackbar
= Snackbar.make(getActivity().findViewById(R.id.content), message, Snackbar.LENGTH_LONG);
snackbar.getView().bringToFront();
snackbar.show();
}
R.id.content is a CoordinatorLayout:
<android.support.design.widget.CoordinatorLayout
android:id="#+id/content"
android:background="?attr/MyActivityBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?attr/headerShadow" />
Is there any good way, to avoid Snackbar from being covered by AutoCompleteTextView's drop down?
I might have a solution for that case. Of course, there are some assumptions but maybe the solution would suit you.
The key here is putting AutoCompleteTextView inside CoordinatorLayout and add custom CoordinatorLayout.Behavior to it.
Create appropriate Behavior for your class:
public class AutoCompleteTextViewBehaviour extends CoordinatorLayout.Behavior<AutoCompleteTextView> {
public AutoCompleteTextViewBehaviour(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, AutoCompleteTextView child, View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
}
Override a method layoutDependsOn:
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, AutoCompleteTextView child, View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
Get reference to AutoCompleteTextView popup view:
Unfortunately I haven't found a simple solution for that. However can be done via reflection.
#Nullable
private View getPopupList(AutoCompleteTextView child) {
try {
Field popupField;
Class clazz;
if (child instanceof AppCompatAutoCompleteTextView) {
clazz = child.getClass().getSuperclass();
} else {
clazz = child.getClass();
}
popupField = clazz.getDeclaredField("mPopup");
popupField.setAccessible(true);
ListPopupWindow popup = (ListPopupWindow) popupField.get(child);
Field popupListViewField = popup.getClass().getDeclaredField("mDropDownList");
popupListViewField.setAccessible(true);
return (View) popupListViewField.get(popup);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
Override onDependentViewChanged method:
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, final AutoCompleteTextView child, View dependency) {
if (popupList == null) {
popupList = getPopupList(child);
if (popupList == null) {
return super.onDependentViewChanged(parent, child, dependency);
}
}
int dropdownBottom = child.getBottom() + child.getDropDownVerticalOffset() + popupList.getHeight();
int snackBarTop = dependency.getTop();
int difference = dropdownBottom - snackBarTop;
if (difference > 0) {
child.setDropDownHeight(popupList.getHeight() - difference);
return true;
} else {
child.setDropDownHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
}
return super.onDependentViewChanged(parent, child, dependency);
}
Apply the behavior to AutocompleteTextView in .xml:
app:layout_behavior="com.example.package.AutoCompleteTextViewBehaviour"/>
Of course this is a very basic solution, that for example does not animate list height, but I think this is a good start. Here is the full gist.
You can calculate and adjust the height of the popup as an alternative.
In the following picture, I am setting the dropdown height as bellow:
textView.viewTreeObserver.addOnGlobalLayoutListener {
textView.dropDownHeight = snackbarView.top - textView.bottom
}
This calculation is for the case where the suggestion list's height is long enough. You might want to set that property to WRAP_CONTENT instead.
As far as I know, it is not possible to change the "z-order" unless you add the SnackBar directly to the WindowManager. AutoCompleteTextView internally is using ListPopupWindow to show the suggestions popup and ListPopupWindow has the window type of WindowManager.LayoutParams.TYPE_APPLICATION_PANEL = 1000 which is higher than the Activity's Window type which is of WindowManager.LayoutParams.TYPE_APPLICATION = 2.
In my case I could get it to work with setZ:
Snackbar snackbar = Snackbar.make(container, text, Snackbar.LENGTH_LONG);
snackbar.getView().setZ(200);
snackbar.show();
Why not just set android:dropDownHeight to a fixed dp value? You can even define it to be dynamically based on screen size by reference a dimension resource. It is one line of code, it solves your problem, easy to maintain and to understand (in six month you will ask yourself, what the whole Behavior stuff is used for).
I think you have to take many thinks in consideration:
there must be space for all. keyboard, snackbar and autocomplete Textview
you cannot change the keyboard height (by your app), so you have to change the height of the autocomplete TextView to be above the snackbar (which is above the keyboard)
LinearLayout bottomSheetViewgroup = (LinearLayout) findViewById(R.id.bottomSheet);
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetViewgroup);
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); //this line
I have this code within my activity's onCreate() method and I'm getting the below NPE exception when the last line is executed:
Caused by: java.lang.NullPointerException:
Attempt to invoke virtual method 'java.lang.Object java.lang.ref.WeakReference.get()' on a null object reference
at android.support.design.widget.BottomSheetBehavior.setState(BottomSheetBehavior.java:440)
While Sanf0rds answer is correct, it doesn't allow the ability to define the BottomSheet as expanded by default. The issue is caused by the WeakReference not being set until the last line of onLayoutChild.
The solution is to provide our own class which extends BottomSheetBehavior, but setting the state inside an overridden onLayoutChild. The code is provided below.
uk/ac/qub/quibe/misc/ExpandedBottomSheetBehavior.java
package uk.ac.qub.quibe.misc;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
/**
* Created by mcp on 15/03/16.
*/
public class ExpandedBottomSheetBehavior<V extends View> extends android.support.design.widget.BottomSheetBehavior<V> {
public ExpandedBottomSheetBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onLayoutChild(final CoordinatorLayout parent, final V child, final int layoutDirection) {
SavedState dummySavedState = new SavedState(super.onSaveInstanceState(parent, child), STATE_EXPANDED);
super.onRestoreInstanceState(parent, child, dummySavedState);
return super.onLayoutChild(parent, child, layoutDirection);
/*
Unfortunately its not good enough to just call setState(STATE_EXPANDED); after super.onLayoutChild
The reason is that an animation plays after calling setState. This can cause some graphical issues with other layouts
Instead we need to use setInternalState, however this is a private method.
The trick is to utilise onRestoreInstance to call setInternalState immediately and indirectly
*/
}
}
In the layout file reference reference your new custom behavior.
Change
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
To
app:layout_behavior="uk.ac.qub.quibe.misc.ExpandedBottomSheetBehavior"
public class ExpandedBottomSheetBehavior<V extends View> extends
android.support.design.widget.BottomSheetBehavior<V> {
public ExpandedBottomSheetBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onLayoutChild(final CoordinatorLayout parent, final V child, final int layoutDirection) {
return super.onLayoutChild(parent, child, layoutDirection);
}
#Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
try {
return super.onInterceptTouchEvent(parent, child, event);
} catch (NullPointerException ignored) {
return false;
}
}
}
The issue with your code is you are trying to call the setState method directly inside onCreate. This is will throw a nullPointer because the WeakReference is not initialized yet. It will get initialized when the Coordinator layout is about to lay its child view.
onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection)
Called when the parent CoordinatorLayout is about the lay out the
given child view.
So the best approach is set the peek height to 0 and show/hide inside the onItemClick listener.
I have answered this question here:
https://stackoverflow.com/a/36236743/1314796
I've found a solution but I still don't known why this happen. The solution is put this last line to user call directly after the activity is running. Ex: in a contextMenu callback or in any OnClickListener.
You can also consider listening to the global layout event, this way you'll be sure that the bottomsheet has been laid out when setting the collapsed state.
final View bottomSheet = findViewById(R.id.bottom_sheet);
bottomSheet.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
bottomSheet.getViewTreeObserver().removeOnGlobalLayoutListener(this);
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
bottomSheetBehavior.setPeekHeight(300);
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
});
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.
I've put a WebView loading an image inside a ViewPager. When I try to scroll the image horizontally I move over to the next view instead of scrolling the image.
Is it possible to make it scroll to the end of the image before moving over to the next view?
#Override
public Object instantiateItem(View view, int i) {
WebView webview = new WebView(view.getContext());
webview.setHorizontalScrollBarEnabled(true);
webview.loadUrl("http://www.site.with.an/image.gif");
((ViewPager) view).addView(webview, 0);
return webview;
}
The following is a real working solution which will scroll the WebView on a horizontal swipe as long as it can scroll. If the WebView cannot further scroll, the next horizontal swipe will be consumed by the ViewPager to switch the page.
Extending the WebView
With API-Level 14 (ICS) the View method canScrollHorizontally() has been introduced, which we need to solve the problem. If you develop only for ICS or above you can directly use this method and skip to the next section. Otherwise we need to implement this method on our own, to make the solution work also on pre-ICS.
To do so simply derive your own class from WebView:
public class ExtendedWebView extends WebView {
public ExtendedWebView(Context context) {
super(context);
}
public ExtendedWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean canScrollHor(int direction) {
final int offset = computeHorizontalScrollOffset();
final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
}
Important: Remember to reference your ExtendedWebView inside your layout file instead of the standard WebView.
Extending the ViewPager
Now you need to extend the ViewPager to handle horizontal swipes correctly. This needs to be done in any case -- no matter whether you are using ICS or not:
public class WebViewPager extends ViewPager {
public WebViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
if (v instanceof ExtendedWebView) {
return ((ExtendedWebView) v).canScrollHor(-dx);
} else {
return super.canScroll(v, checkV, dx, x, y);
}
}
}
Important: Remember to reference your WebViewPager inside your layout file instead of the standard ViewPager.
That's it!
Update 2012/07/08: I've recently noticed that the stuff shown above seems to be no longer required when using the "current" implementation of the ViewPager. The "current" implementation seems to check the sub views correctly before capturing the scroll event on it's own (see canScroll method of ViewPager here). Don't know exactly, when the implementation has been changed to handle this correctly -- I still need the code above on Android Gingerbread (2.3.x) and before.
Although Sven mentioned for layout file I want to add detail. After you extend Webview and ViewPager classes,
Inside your activity you will cast to your extended class like this:
web = (MyWebView) findViewById(R.id.webview);
Inside your layout file like this:
<your.package.name.MyWebView
android:id="#+id/webview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>