Collapse header on scrolling without using CollapsingToolbarLayout - android

I'm trying to implement the same behaviour than the CollapsingToolbarLayout for a view above my recyclerview that collapses to a minimum height on downscrolling and restores to its normal height when the scroll reaches the top. This header is not a Toolbar, hence the custom view.
I managed to do it, but when scrolling up it restores the height but the whole view starts flickering.
This is the header layout, I am trying to make the Textview disappear while keeping the inner linear layout visible at all times:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="#+id/welcome_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:text="welcome"/>
<LinearLayout
android:id="#+id/always_show"
android:layout_width="match_parent"
android:layout_height="36dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_margin="12dp">
...
</LinearLayout>
</LinearLayout>
And this is the code
var maxHeight: Int = 0
var initialSize = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
header.getViewTreeObserver().addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (initialSize)
return
initialSize = true
maxHeight = header.measuredHeight
}
})
recyclerView.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy > 0) {
//scroll down
val params = header.layoutParams as LinearLayout.LayoutParams
params.topMargin = Math.max(params.topMargin-dy, -maxHeight/2)
header.layoutParams = params
} else if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
//top
val params = header.layoutParams as LinearLayout.LayoutParams
params.topMargin = 0
header.layoutParams = params
}
}
})
}
Any ideas on how to make this animation more natural, closest to the CollapsingToolbarLayout behaviour.

Related

How to make bottom sheet (BottomSheetBehavior) expandable to arbitrary position?

I have bottom sheet, and I want to change its behavior so it would work like on the main screen of Google Maps application, where you can expand it to any position and leave it there and it won't automatically stick to the bottom or to the top. Here's my layout with bottom sheet:
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.gms.maps.MapView
android:id="#+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<View
android:id="#+id/shadow"
android:layout_width="match_parent"
android:layout_height="16dp"
android:background="#drawable/shape_gradient_top_shadow"
app:layout_anchor="#+id/map_bottom_sheet" />
<LinearLayout
android:id="#+id/map_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="300dp"
android:fillViewport="false"
android:orientation="vertical"
app:behavior_peekHeight="50dp"
android:background="#color/lightGray"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<include layout="#layout/bottom_sheet_top_buttons"/>
<android.support.v4.widget.NestedScrollView
android:id="#+id/bottom_sheet_content_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/lightGray"/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
What I need in essence is eliminate forcing of STATE_EXPANDED and STATE_COLLAPSED states when dragging is ended.
Here's a visual explanation of what I try to achieve:
As you can see, bottom sheet doesn't automatically anchor to the top or the bottom but stays at whatever position it was left.
Hi Alex you can try this code for similar expected behaviour, it is not as optimised but it will help you to understand the concept.
final DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
bottomSheetBehavior.setPeekHeight(200);
// set callback for changes
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
Log.d(TAG, "onStateChanged: " + bottomSheet.getY() + "::" + bottomSheet.getMeasuredHeight() + " :: " + bottomSheet.getTop());
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
ViewGroup.LayoutParams params = bottomSheet.getLayoutParams();
params.height = Math.max(0, metrics.heightPixels - (int) bottomSheet.getTop());
bottomSheet.setLayoutParams(params);
}
});
Copy the code from android.support.design.widget.BottomSheetBehavior to make your own custom behavior. Then modify the onViewReleased() method which is responsible for the movement of the sheet after the drag ends. You also have to introduce a new state besides the existing ones - the state is helpful to restore the position and let others know in which state your sheet is at the moment with getState().
#Override
public void onViewReleased(View releasedChild, float xVel, float yVel) {
int top;
#State int targetState;
// Use the position where the drag ended as new top
top = releasedChild.getTop();
// You have to manage the states here, too (introduce a new one)
targetState = STATE_ANCHORED;
if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) {
setStateInternal(STATE_SETTLING);
ViewCompat.postOnAnimation(releasedChild, new SettleRunnable(releasedChild, targetState));
} else {
setStateInternal(targetState);
}
}
I have created a proof of concept originating from the orginal source code from the design library. You can view it here. The problem with the original behavior is it doesn't allow flings, and most methods are private so extending the class and overriding some methods in an attempt to achieve it won't get you very far either. My implementation allows for optional snapping behavior, transient states (don't automatically snap after drag) and customizations around setting peek height and max height.

android set margin programmatically

after i press a button, i would like to move a textfield programmatically from the actual position + 20 margin left.
this is my xml file:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:paddingBottom="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/txtField"
android:layout_below="#+id/txtField2"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="20dp"
android:layout_toLeftOf="#+id/Seperator"
android:layout_toStartOf="#+id/Seperator"
android:layout_marginRight="10p" />
....
</RelativeLayout>
i tried this code:
parameter = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
parameter.setMargins(30, 32, 10, 0); // left, top, right, bottom
txtField.setLayoutParams(parameter);
this works semi optimal.
is there an way to use all the values of an xml file and only change the margin left value programmatically?
Try this code:
parameter = (RelativeLayout.LayoutParams) txtField.getLayoutParams();
parameter.setMargins(leftMargin, parameter.topMargin, parameter.rightMargin, parameter.bottomMargin); // left, top, right, bottom
txtField.setLayoutParams(parameter);
Yep,
Take for example the following method that updates the left margin of a given view:
public static void setMarginLeft(View v, int left) {
ViewGroup.MarginLayoutParams params =
(ViewGroup.MarginLayoutParams)v.getLayoutParams();
params.setMargins(left, params.topMargin,
params.rightMargin, params.bottomMargin);
}
You can also try
ImageView _circle=(ImageView) findViewById(R.id.image);
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) _circle.getLayoutParams();
int _20 = (int)getResources().getDimension(R.dimen.image_margin);
// Directly set 20
mlp.setMargins(_20, _20, _20, _20);
If you use kotlin, this can be simplified by creating an extension function
fun View.setMarginLeft(leftMargin: Int) {
val params = layoutParams as ViewGroup.MarginLayoutParams
params.setMargins(left, params.topMargin, params.rightMargin, params.bottomMargin)
layoutParams = params
}
Now all you need is a view, and this extension function can be used anywhere.
val textView = findViewById(R.id.textView)
val marginLeft = context.resources.getDimension(R.dimen.marginLeft).toInt()
textView.setMarginLeft(marginLeft)
Note: This extension function will leave all other margins intact, no margins will be overwritten

ViewAnimationUtils - No animation occurs

I'm trying to use a Circular Reveal to animate in a LinearLayout on click of a FAB.
Onclick, the view changes to become visible after a time delay, but no visible animation runs? In addition, when the function is reversed as an exit animation the view is invisible for the duration of the animation.
The code:
//Rest of method triggered onClick Above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
circleReveal(formcontainer,addnewbutton);
ObjectAnimator.ofFloat(addnewbutton, "translationY", -heightbutton).start();
}
}
public void circleReveal(LinearLayout formLayout, FloatingActionButton fab) {
int cx = (fab.getLeft() + fab.getRight()) / 2;
int cy = (fab.getTop() + fab.getBottom()) / 2;
int radius = Math.max(formLayout.getWidth(), formLayout.getHeight());
Animator reveal = createCircularReveal(formLayout, cx, cy, 0, radius);
formLayout.setVisibility(View.VISIBLE);
reveal.setDuration(2000);
reveal.start();
}
Many thanks in advance for your help!
[EDIT] After testing, i've noticed that it's just this view that it fails on? Every other view in my layout that i've tried works fine?
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/formcontainer"
android:background="#android:color/white"
android:layout_width="match_parent"
android:layout_height="65dp"
android:orientation="vertical"
android:baselineAligned="false"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:gravity="bottom"
android:layout_marginBottom="0dp"
android:elevation="4dp"
android:layout_alignParentBottom="true">
[EDIT 2] So, i've partially solved the issue, in that the animation now works, but only when the centre is not calculated from the FAB?
The circular animation runs with respect to the View itself. So, your center should be:
int cx = fab.getWidth() / 2;
int cy = fab.getHeight() / 2;

Resolving conflict between android:gravity and android:layout_gravity

Consider the following trivial layout.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="top" >
<Button
android:id="#+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>
</LinearLayout>
The outer LinearLayout specifies that it has
android:gravity="bottom"
which means that it wants its children to "fall to the bottom".
The inner LinearLayout specifies that
android:layout_gravity="top"
which means that it wants itself to be laid out at the top of its enclosing layout. (To specify android:layout_gravity one must apparently edit the XML file directly. There does not seem to be a way to reach that attribute from Eclipse. Anyhow...)
How does Android resolve such a conflict? The example above suggests that the gravity attribute overrides the layout_gravity attribute. Is that how the conflict is resolved in general?
From taking a look at the layoutVertical() method in LinearLayout, the gravity of the LinearLayout itself is what determines how the children are positioned.
The layout_gravity of each child is used only for laying out "in the other direction" (i.e. left-right).
First:
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
childTop = ...
break;
// ... same for CENTER_VERTICAL and TOP
}
Then, for each child:
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = ...
break;
// ... same for RIGHT and LEFT
And equivalently for layoutHorizontal().
Also, just a comment but android:layout_gravity can be accessed from Java code. It's the gravity field in the LinearLayout.LayoutParams class (which is set as the LayoutParams of the child view).

Stop scrolling on the bottom sheet when it gets hidden and always show the top when it reappears

It seems that if I hide the bottom sheet while it is scrolling fast (with animation) and show the bottom sheet again soon, it keeps scrolling. Can I disable this, and when it reappears can I make it always show the top of the bottom sheet reliably?
Steps to reproduce
Press the "Show" button.
Scroll up the bottom sheet until it fills the entire activity.
Swipe up fast so that the text view scrolls down fast with animation.
While it is scrolling (showing about Line 170 or something), press the back button to hide it.
Quickly press the "Show" button again to show the bottom sheet again.
Expected behaviour
It shows Line 0 without scrolling.
Actual behaviour
It keeps scrolling and showing Line 50 or something.
Activity source
class MainActivity : AppCompatActivity()
{
var mBottomSheet:BottomSheetBehavior<NestedScrollView>?= null;
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mBottomSheet = BottomSheetBehavior.from(bottomSheet);
mBottomSheet?.peekHeight = 600;
mBottomSheet?.state = BottomSheetBehavior.STATE_HIDDEN;
button.setOnClickListener {
if(mBottomSheet?.state == BottomSheetBehavior.STATE_HIDDEN)
{
mBottomSheet?.state = BottomSheetBehavior.STATE_COLLAPSED;
bottomSheet.scrollX = 0;
bottomSheet.scrollTo(0, 0);
val sb = StringBuilder();
for (line in 0..300)
{
sb.appendln("Line $line");
}
textView.text = sb.toString();
}
}
}
override fun onBackPressed()
{
if(mBottomSheet?.state != BottomSheetBehavior.STATE_HIDDEN)
{
bottomSheet.stopNestedScroll();
textView.stopNestedScroll();
mBottomSheet?.state = BottomSheetBehavior.STATE_HIDDEN;
}
else
{
super.onBackPressed()
}
}
}
Layout code
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.loser.bsproblem.MainActivity">
<Button
android:id="#+id/button"
android:layout_marginTop="50dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="Show the bottom sheet"
/>
<android.support.v4.widget.NestedScrollView
android:id="#+id/bottomSheet"
android:background="#EEEEFF"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
app:behavior_hideable="true"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textView"
android:textAppearance="#style/Base.TextAppearance.AppCompat.Large"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
When it first appears. I want it to be always shown like this when it appears.
When the problem above happens.

Resources