I am working on an Android 4+ app which uses a quite simply layout: Multiple views are stacked using a LinearLayout within a ScrollView
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="HardcodedText,UseCompoundDrawables,UselessParent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
<!-- Top Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
<Button... />
</LinearLayout>
<!-- Hidden Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
... Some Content ...
</LinearLayout>
<!-- Bottom Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
... Some Content ...
</LinearLayout>
</LinearLayout>
</ScrollView>
The HiddenContainer should not be visible when the layout it created. Thus in the beginning the BottomContainer is directly beneath the TopContainer. A click on the Button within the TopContainer toggles the visibility of the HiddenContainer.
Doing this with hiddenContainer.setVisibility(hiddenContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE) works find and without any problem. However it does not look good when the view suddenly appears or disappears. Instead I would like to animate the change.
I was surprised that I was not able to find an easy solution for this:
Using android:animateLayoutChanges="true" does work, however I am not able to control the animation.
While using a ValueAnimator to change hiddenContainer.setScaleY(...) gives me control over the animation setScaleY(0) makes the container invisible without reducing the space it occupies within the layout.
Using the ValueAnimator to change hiddenContainer.setHeight(...) might work, however I don't want to use a fixed height value when showing the container (e.g. hiddenContainer.setHeight(300)) but the height which is determined by the containers content.
So, how to solve this?
For animate your changes of layout (alpha, visibility, height, etc) you can use TransitionManager. For example: I have three static methods and use them when I want to animate layout changes:
public static final int DURATION = 200;
public static void beginAuto(ViewGroup viewGroup) {
AutoTransition transition = new AutoTransition();
transition.setDuration(DURATION);
transition.setOrdering(TransitionSet.ORDERING_TOGETHER);
TransitionManager.beginDelayedTransition(viewGroup, transition);
}
public static void beginFade(ViewGroup viewGroup) {
Fade transition = new Fade();
transition.setDuration(DURATION);
TransitionManager.beginDelayedTransition(viewGroup, transition);
}
public static void beginChangeBounds(ViewGroup viewGroup) {
ChangeBounds transition = new ChangeBounds();
transition.setDuration(DURATION);
TransitionManager.beginDelayedTransition(viewGroup, transition);
}
And when you want to animate layout changes you can just call one of this methods before layout changings:
beginAuto(hiddenContainerParentLayout);
hiddenContainer.setVisibility(hiddenContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE)
Related
I would like to achieve the behavior when adding or removing the items to/from the recycler view, it changes its height according to the content up to the specified #dimen/maxRecyclerViewHeight value with smooth animation. It's working fine without animation after notifyItemInserted/Removed but the views under the recycler view are 'jumping' so that looks a bit odd. Can I achieve that somehow using TransitionManager.beginDelayedTransition(...)? I appreciate any other ideas.
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/constraint_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constrainedHeight="true"
app:layout_constraintHeight_max="#dimen/maxRecyclerViewHeight"/>
<!-- Other views go under the recycler view -->
</android.support.constraint.ConstraintLayout>
After doing some research I found this can definitely be achieved with TransitionManager.
Let's assume the xml file containing the constraint layout with the recycler view inside is activity_example.xml. So immediately after invoking notifyItemInserted/notifyItemRemoved and notifyItemRangeChanged you can call something like method below:
private void beginDelayedTransition() {
final ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(this, R.layout.activity_example);
// this transition is responsible for animation resizing the view
final Transition transition = new ChangeBounds();
// or any other duration
transition.setDuration(250);
// constraintLayout is a root layout in the activity_example.xml
TransitionManager.beginDelayedTransition(constraintLayout, transition);
constraintSet.applyTo(constraintLayout);
}
I have a BottomSheetView which has animateLayoutChanges="true". Initially it shows up fine. But if change the visibility of a view (inside BottomSheetView) from GONE to VISIBLE, the app messes up calculations and my BottomSheetView moves to the top of the screen. i have tried setting layout_gravity=bottom at the root of the BottomSheetView layout. But no success.
Here I have the image of my BottomSheetView before changing the visibility of any view. (Click image for full size)
After I change the visibility of a view (GONE to VISIBLE or VISIBLE to GONE), my BottomSheetView moves to the top. (Click image for full size)
I guess, Android is messing up while making calculations about the measurement of view width and height. Any way to solve this??
I also tried to make my BottomSheetView extend fully to match the parent view, but somehow that is making the height of the BottomSheetView longer than the phone screen and in-tun creating scrolling issues.
Expected solutions:
1> Prevent BottomSheetView from changing its position even when the visibility of a view is changed.
OR
2>Make the BottomSheetView match parent so that it does not look bad after messing up with the calculations.
The BottomSheetBehavior does not work well with LayoutTransition (animateLayoutChanges="true") for now. I will work on a fix.
For now, you can use Transition instead. Something like this will fade the view inside and animate the size of the bottom sheet.
ViewGroup bottomSheet = ...;
View hidingView = ...;
TransitionManager.beginDelayedTransition(bottomSheet);
hidingView.setVisibility(View.GONE);
You can refer to Applying a Transition for more information including how to customize the animation.
I was running into the same issue and determined to find a fix. I was able to find the underlying cause but unfortunately I do not see a great fix at the moment.
The Cause:
The problem occurs between the bottomsheet behavior and the LayoutTransition. When the LayoutTransition is created, it creates a OnLayoutChangeListener on the view so that it can capture its endValues and setup an animator with the proper values. This OnLayoutChangeListener is triggered in the bottomSheetBehavior's onLayout() call when it first calls parent.onLayout(child). The parent will layout the child as it normally would, ignoring any offsets that the behavior would change later. The problem lies here. The values of the view at this point are captured by the OnLayoutChangeListener and stored in the animator. When the animation runs, it will animate to these values, not to where your behavior defines. Unfortunately, the LayoutTransition class does not give us access to the animators to allow updating of the end values.
The Fix:
Currently, I don't see an elegant fix that involves LayoutTransitions. I am going to submit a bug for a way to access and update LayoutTransition animators. For now you can disable any layoutTransition on the parent container using layoutTransition.setAnimateParentHierarchy(false). Then you can animate the change yourself. I'll update my answer with a working example as soon as I can.
The question was asked more than two years ago, but unfortunately the problem persists.
I finally got a solution to keep the call to the addView and removeView functions in a BottomSheet, while having animateLayoutChanges="true".
BottomSheetBehavior cannot calculate the correct height when it changes, so the height must remain the same. To do this, I set the height of the BottomSheet to match_parent and divide it into two children: the content and a Space that changes height according to the height of the content.
To best mimic the true behavior of a BottomSheet, you also need to add a TouchToDismiss view that darkens the background when the BottomSheet is extended but also to close the BottomSheet when the user presses outside the content.
Here's the code:
activity.xml
<androidx.coordinatorlayout.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=".MainActivity">
<Button
android:id="#+id/show_bottom_sheet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Show bottom sheet"/>
<View
android:id="#+id/touch_to_dismiss"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:background="#9000"/>
<LinearLayout
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<Space
android:id="#+id/space"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:id="#+id/bottom_sheet_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:animateLayoutChanges="true">
<Button
android:id="#+id/add_or_remove_another_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add another view"/>
<TextView
android:id="#+id/another_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Another view"/>
</LinearLayout>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
activity.java
BottomSheetBehavior bottomSheetBehavior;
View touchToDismiss;
LinearLayout bottomSheet;
Button showBottomSheet;
Space space;
LinearLayout bottomSheetContent;
Button addOrRemoveAnotherView;
TextView anotherView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
touchToDismiss = findViewById(R.id.touch_to_dismiss);
touchToDismiss.setVisibility(View.GONE);
touchToDismiss.setOnClickListener(this);
bottomSheet = findViewById(R.id.bottom_sheet);
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
bottomSheetBehavior.setPeekHeight(0);
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
#Override
public void onStateChanged(#NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN || newState == BottomSheetBehavior.STATE_COLLAPSED) {
touchToDismiss.setVisibility(View.GONE);
}else {
touchToDismiss.setVisibility(View.VISIBLE);
}
}
#Override
public void onSlide(#NonNull View bottomSheet, float slideOffset) {
touchToDismiss.setAlpha(getRealOffset());
}
});
showBottomSheet = findViewById(R.id.show_bottom_sheet);
showBottomSheet.setOnClickListener(this);
space = findViewById(R.id.space);
bottomSheetContent = findViewById(R.id.bottom_sheet_content);
addOrRemoveAnotherView = findViewById(R.id.add_or_remove_another_view);
addOrRemoveAnotherView.setOnClickListener(this);
anotherView = findViewById(R.id.another_view);
bottomSheetContent.removeView(anotherView);
}
#Override
public void onClick(View v) {
if (v == showBottomSheet)
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
else if (v == addOrRemoveAnotherView) {
if (anotherView.getParent() == null)
bottomSheetContent.addView(anotherView);
else
bottomSheetContent.removeView(anotherView);
}
else if (v == touchToDismiss)
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
/**
* Since the height does not change and remains at match_parent, it is required to calculate the true offset.
* #return Real offset of the BottomSheet content.
*/
public float getRealOffset() {
float num = (space.getHeight() + bottomSheetContent.getHeight()) - (bottomSheet.getY() + space.getHeight());
float den = bottomSheetContent.getHeight();
return (num / den);
}
This is the result obtained with this code:
Hopefully it will be useful to someone since the problem is still there!
I use this and I got to keep the animations!
val transition = LayoutTransition()
transition.setAnimateParentHierarchy(false)
{Parent View which has animateLayoutChanges="true" }.layoutTransition = transition
In the BottomSheetDialog default layout (design_bottom_sheet_dialog) there is a TOP gravity on the dialog's design_bottom_sheet FrameLayout:
android:layout_gravity="center_horizontal|top"
I don't really know why on BottomSheetDialog gravity is top.
You need to create the same layout file (same content and name) in your project and replace this line with:
android:layout_gravity="center_horizontal|bottom"
In our case, we were displaying a progress bar on a button component. This was hiding the button text and displaying progress bar. This was causing a jump in bottom sheet fragment. Using Invisible instead of Gone fixed the problem.
If your view allows, make the visibility INVISIBLE instead of GONE.
See bottom of this entry for an answer to this "problem".
In my app I inflate some xml views and add them to a LinearLayout, list, within a ScrollView. The XML looks like this:
<ScrollView
android:layout_width="0dp"
android:layout_weight="19"
android:layout_height="wrap_content"
android:fillViewport="true">
<LinearLayout
android:id="#+id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
I then try to give focus to one of the views I've put into 'list'. This is done by using this code :
view.getParent().requestChildFocus(view, view);
If the above code results in a scroll down, the focused view get placed at the bottom of the screen and if it result in a scroll up, the focused view get placed at the top of the screen.
Is there a way to make the focused view always get placed at the top of the screen if the length of the list permits it?
Edit: This works! See accepted answer.
XML
<ScrollView
android:id="#+id/scroll_list"
android:layout_width="0dp"
android:layout_weight="19"
android:layout_height="wrap_content"
android:fillViewport="true">
<LinearLayout
android:id="#+id/list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
Sample code to put somewhere in your activity or fragment
view = findViewById(get_view_that_should_have_focus);
ScrollView scrollView = findViewById(R.id.scroll_list);
scrollView.smoothScrollTo(0, view.getTop());
You can vertical scroll to specific view by using this:
scrollView.smoothScrollTo(0,view.getTop());
It will make your view top of the scrollview(if there is enough height) After scrolling, you can request focus for this view.
Since I can't comment, I'm just gonna post this here.
The solution provided by Oğuzhan Döngül is correct, but if some of you can't get it running, try to do the smoothScroll inside runnable :
val topOffset = view.height
scrollView.post {
scrollView.smoothScrollTo(0, view.top - topOffset)
}
I add topOffset to give some spaces on top of the highlighted view.
source
I've got such a layout:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ScrollView
android:id="#+id/scroll_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:fillViewport="true" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
...
other views
...
<TextView
android:id="#+id/text_view_that_can_change"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
</ScrollView>
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
layout="#layout/btns_ok_cancel" />
</RelativeLayout>
The problem is that when I add text to initially empty text_view_that_can_change - my ScrollView scrolls to the top.
The same problem happens when I set visibility to some of views inside ScrollView/RelativeLayout.
I can scroll it back using scrollView.scrollTo(0, yScroll) but it gives a visible and, I must say, ugly jerk to the whole content.
It there any way to prevent this behavior?
Have spent some hours and here is what I've discovered.
Scroll to the top happens because inner RelativeLayout is being re-laid out as a result of the content change.
You should add a couple of lines to control this situation:
1) declare class member that will hold scroll position:
int mSavedYScroll;
2) Add GlobalLayoutListener to the main view (here it's a fragment's main view, but that doesn't matter):
public View onCreateView(...) {
mView = inflater.inflate(...);
...
mView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
if (mSavedYScroll != 0) {
mScrollContainer.scrollTo(0, mSavedYScroll);
mSavedYScroll = 0;
}
}
});
}
3) When you do something (anywhere in you code) that may change content of ScrollView - just save current scroll position:
mSavedYScroll = mScrollContainer.getScrollY();
//DO SOMETHING THAT MAY CHANGE CONTENT OF mScrollContainer:
...
That's it!
I'm trying to animate a view and hide it after some DP's were scrolled and i made everything fine, but the problem is that it will flick horribly when you are scrolling slowly before or after the Y value that is supposed to trigger the animation.
I think the flick is because i have to set its visibility to Gone and update the other view as match_parent, it won't work with just the TraslationY:
view.animate()
.translationY(-view.getBottom())
.alpha(0)
.setDuration(HEADER_HIDE_ANIM_DURATION)
.setInterpolator(new DecelerateInterpolator());
I tried to set the layout to relative and View 2 as match_parent to see if i could avoid the visibility change but it didn't work...
I have implemented all required code from Google I/O 2014 BaseActivity.java file:
https://github.com/google/iosched/blob/master/android/src/main/java/com/google/samples/apps/iosched/ui/BaseActivity.java#L17
And the animation works... but i assume that, as my customview isn't an actionbar with overlay properties, the customview won't leave and LinearLayout below won't fill the empty space (there is none).
SO, i made it to work with an animationlistener and setting customview visibility to gone when the animation is over but it will flick in a horrible way when you are close to the expected Y point that trigger the animation (flick as customview visibility is gone and LinearLayout below needs to resize itself to fill the empty space, and will quickly repeat if you scroll slowly around there).
XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:animateLayoutChanges="true">
<com.project.app.layouts.TabsLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/tabs">
<FrameLayout
android:id="#+id/content_frame_header"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.v4.view.ViewPager
android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:background="#android:color/white"/>
</LinearLayout>
</RelativeLayout>
Is there any way to do this when it's not an actionbar?
EDIT:
Added the comment about how it will flick when you scroll slowly around Y point that triggers the animation to hide/show.
I recommend you to use android:hardwareAccelerated="true" attribute in your Manifest file. It will use your device's GPU to draw views and animations.
I suggest you to check the value of view.getBottom() in both cases (when it works and when not).
It may be that it flicks because the value returned by view.getBottom() is very big.
Add this line in your code:
Log.i("YourAppName", "view.getBottom(): -" + view.getBottom());
view.animate()
.translationY(-view.getBottom())
.alpha(0)
.setDuration(HEADER_HIDE_ANIM_DURATION)
.setInterpolator(new DecelerateInterpolator());
Then check your log to see if the values are the same or not.
I have made it in a slightly different way. Note that the call I'm using requires SDK 19 (KitKat), but you can still do it using ViewPropertyAnimatorCompat
I have a FrameLayout that has the header view and the main view, with the header view on front.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<include layout="#layout/main_layout" android:id="#+id/main_layout" />
<include layout="#layout/header_layout" android:id="#+id/header_layout" />
</FrameLayout>
Once the views are measured (posting a runnable during onResume) I set the topPadding of the main view to be the Height of the header.
In the hide and show animation, I add an update listener and inside it I update the top padding of the main view to be the Height of the header + the Translation on Y.
final View header = findViewById(R.id. header_layout);
header.animate()
.translationY(-header.getBottom())
.setDuration(200)
.setInterpolator(new DecelerateInterpolator())
.setUpdateListener(new AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
int top = (int) (header.getHeight()+header.getTranslationY());
findViewById(R.id.main_view).setPadding(0, top, 0, 0);
}
});
This makes it a bit smoother, since the padding gets updated together with the translation.
Actually, setting an AnimationListener using ViewPropertyAnimatorCompat does not work. The listener is never called, so for backwards compatibility I opted for this solution, not elegant, but at least it work on pre-KitKat devices:
final View mainView = findViewById(R.id.main_view);
Runnable mainUpdateRunnable = new Runnable() {
#Override
public void run() {
int top = (int) (header.getHeight()+header.getTranslationY());
mainView.setPadding(0, top, 0, 0);
if (mAnimatingHeader) {
mainView.post(this);
}
}
};
mainView.post(mainUpdateRunnable);
The variable mAnimatingHeader is updated using the AnimationListener (which works)