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);
}
});
Related
I am recently using CustomBottomSheetBehavior to make an googlemaps like bottom sheet behavior and it works great. I have only one problem.please look at this image
If I use it in a scrolling activity the content of tool-bar covers my list box. so I ahve to add margin-top to my list view. It works but when I draw bottomsheet up toolbar goes up and behind it, there is an empty space. This is because I have added some margin top to make my list's top visible. Is there any way to connect list's margin top to the amount of moving bottom-sheet and when It moves up decrease margin value to and when it moves down increase it?or is there any better way ?
It seems I have to develope my own TopMarginBehavior for this job but I have no idea how to do it.
thanks
Create your own class related to the behavior you want (MarginTopBehavior)
Extends it from CoordinatorLayout.Behavior
Now you have to focus on 2 methods: layoutDependsOn and onDependentViewChanged. With the first one you are selecting the view that your MarginTopBehavior is following, in this case is a NestedScrollView. With the second one you are reacting (the magic!) when the scroll get moved.
At this point you get this:
public class MarginTopBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
private FrameLayout.LayoutParams mLayoutParams;
public MarginTopBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof NestedScrollView;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
}
}
The logic to be applied in onDependentViewChanged is just this:
* Define the cap (min/max margin value) and controls when the margin value has reached one of those cap.
* Update margin value while the values are between the caps. In this point you have to implement an algorithm about what you want (parallax, linear, etc). That is what I'm calling THE_MAGIC_ECC in the next code:
public class MarginTopBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
/**
* Params of the component you want to modify the margin
*/
private FrameLayout.LayoutParams mLayoutParams;
/**
* Used to access DIMENS in your project
*/
private Context mContext;
private int mMinYvalue;
private int mMaxYValue;
public MarginTopBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof NestedScrollView;
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (mLayoutParams == null) {
mLayoutParams = (FrameLayout.LayoutParams) child.getLayoutParams();
}
if (dependency.getY() <= mMinYvalue) {
mLayoutParams.setMargins(0, 0, 0, 0);
child.setLayoutParams(mLayoutParams);
return true;
}
else if (dependency.getY() > mMinYvalue && dependency.getY() <= mMaxYValue) {
int THE_MAGIC_ECC = 1 + 2 + 3;
mLayoutParams.setMargins(0, 0, 0, THE_MAGIC_ECC );
child.setLayoutParams(mLayoutParams);
return true;
}
else {
mLayoutParams.setMargins(0, 0, 0, 100);
child.setLayoutParams(mLayoutParams);
return true;
}
}
}
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;
}
My layout is absolutely identical with this tutorial. But in this tutorial we use app:layout_behavior="pl.michalz.hideonscrollexample.ScrollingFABBehavior"
in Fab XML, so SnackBar covers the Fab. Without this code Fab does not move followed by RecyclerView. How to show SnackBar correctly?
Snackbar.make(getActivity().findViewById(R.id.coordinatorLayout),
adapter.getNewsList().get(position).getTitle(), Snackbar.LENGTH_LONG).show();
The example you follow is pretty unlucky. The default behavior of FloatingActionButton within CoordinatorLayout is to move up when you display SnackBar. Since this code overrides the Behavior you lose this feature because the methods never call their super class implementations. Clearly the author have not thought about this. However, you can modify the ScrollingFABBehavior to extend the original Behavior and thus support SnackBar:
public class ScrollingFABBehavior extends FloatingActionButton.Behavior {
private int toolbarHeight;
public ScrollingFABBehavior(Context context, AttributeSet attrs) {
super();
this.toolbarHeight = Utils.getToolbarHeight(context);
}
#Override
public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton fab, View dependency) {
return super.layoutDependsOn(parent, fab, dependency) || (dependency instanceof AppBarLayout);
}
#Override
public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton fab, View dependency) {
boolean returnValue = super.onDependentViewChanged(parent, fab, dependency);
if (dependency instanceof AppBarLayout) {
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
int distanceToScroll = fab.getHeight() + fabBottomMargin;
float ratio = (float)dependency.getY()/(float)toolbarHeight;
fab.setTranslationY(-distanceToScroll * ratio);
}
return returnValue;
}
}
This is actually the class from the example's github repository, I have found it just after I coded the same myself and wanted to test it. They only forgot to update the blog post :-/
Have you tried this?
...
View view = inflater.inlfate(R.layout.my_layout, parent, false);
...
Snackbar.make(view.findViewById(R.id.fab),
adapter.getNewsList().get(position).getTitle(), Snackbar.LENGTH_LONG).show();
...
return view;
I assume that you call above code in Fragment, so I added view variable to call the findViewById() method.
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.
Background
I have a layout that has some special states (like being checked/pressed), and I wish to set its children to apply their own drawables based on this layout.
i'm searching for an alternative of setting duplicateParentState to true for each of its children (and maybe even all of its descendants).
What I've tried
I've tried to make the custom view have an attribute of setting it to each of its children, but i couldn't find in which method call to apply this attribute to all of its children. in each method i've tried, it either returns 0 for getChildCount() or it just doesn't do anything to the child itself ( using setDuplicateParentStateEnabled() ) .
The problem
as the documentation says , using setDuplicateParentStateEnabled won't do anything on the cases i need it from :
Note: in the current implementation, setting this property to true
after the view was added to a ViewGroup might have no effect at all.
This property should always be used from XML or set to true before
adding this view to a ViewGroup.
so it seems i use it too late, but i need to call it late since the children don't exist yet in the parent...
The question
How can I achieve this functionality of avoiding setting duplicateParentState for each child, and just set it to the parent view?
How about extending the widget -like you said- and overriding onLayout()? This way you ensure to modify the children state also the first time the widget is drawn. When onLayout() is called, getChildCount() will always return the actual number of children. For example:
public class LinearLayout extends android.widget.LinearLayout {
private static final int[] VIEW_GROUP_ATTRS = new int[]{ android.R.attr.enabled };
private static final boolean DEFAULT_IS_ENABLED = true;
private boolean isEnabled = DEFAULT_IS_ENABLED;
public LinearLayout(Context context) {
super(context);
}
public LinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
isEnabled = context.obtainStyledAttributes(attrs, VIEW_GROUP_ATTRS).getBoolean(0, DEFAULT_IS_ENABLED);
}
#Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
this.isEnabled = enabled;
for (int i = 0; i < getChildCount(); ++i) {
getChildAt(i).setEnabled(enabled);
}
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
setEnabled(isEnabled);
}
}
Note that you are actually extending the widget (calling super on any method being overridden).