如何将视图移动到Snackbar上方,就像浮动按钮一样

49

我有一个线性布局,在Snackbar出现时想要将其向上移动。

我看到了很多用FloatingButton来完成这个操作的例子,但普通视图该怎么做?


18
使用 app:layout_dodgeInsetEdges="bottom" - Mygod
1
@Mygod 你应该将这个作为一个答案添加进去。 - Mokkun
@もっくん 我已经回复了。由于回复太短而优雅,Stackoverflow自动将其转换为评论。 :) - Mygod
我应该在哪里添加这个@Mygod? - arniotaki
9个回答

79

我将进一步阐述已批准的答案,因为我认为有一个比该文章提供的略微简单的实现方法。

我无法找到处理视图通用移动的内置行为,但这是一个很好的通用选项(来自http://alisonhuang-blog.logdown.com/posts/290009-design-support-library-coordinator-layout-and-behavior的链接,由另一条评论链接):

import android.content.Context;
import android.support.annotation.Keep;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;

@Keep
public class MoveUpwardBehavior extends CoordinatorLayout.Behavior<View> {

    public MoveUpwardBehavior() {
        super();
    }

    public MoveUpwardBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency instanceof Snackbar.SnackbarLayout;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        float translationY = Math.min(0, ViewCompat.getTranslationY(dependency) - dependency.getHeight());
        ViewCompat.setTranslationY(child, translationY);
        return true;
    }

    //you need this when you swipe the snackbar(thanx to ubuntudroid's comment)
    @Override
    public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
        ViewCompat.animate(child).translationY(0).start();
    }
}

然后,在您的布局文件中添加以下布局行为:

<LinearLayout
    android:id="@+id/main_content"
    android:orientation="vertical"
    app:layout_behavior="com.example.MoveUpwardBehavior"/>

layout_behavior指的是您自定义行为的完整路径。除非您需要默认行为,否则无需对LinearLayout进行子类化,这似乎并不常见。


12
这种解决方案不需要对目标视图进行子类化,我认为这样做有些过度。此外,通常最好在行内解释答案,因为链接往往会在一段时间后失效。 - Travis Castillo
9
很好的解决方案。若要支持滑动关闭 snackbar ,请添加以下代码:@Override public void onDependentViewRemoved ( CoordinatorLayout parent, View child, View dependency ) { super.onDependentViewRemoved(parent, child, dependency); child.setTranslationY(0); } - ubuntudroid
5
如果你想要一个依赖视图的实际动画效果,可以使用child.animate().translationY(0)来替代child.setTranslation(0) - ubuntudroid
4
Snackbar.make(..) 方法需要传入一个 View,这个 View 必须是 CoordinatorLayout 或者是 CoordinatorLayout 的子视图。如果像我一样使用了 getRootView() 作为参数,就会导致 layoutDependsOn(..) 永远返回 false。请注意使用正确的参数。 - Espen Riskedal
2
ViewCompat.getTranslationY(dependency) is deprecated now, Use dependency.getTranslationY() instead; Similarly use child.setTranslationY(translationY) - Onkar Nene
显示剩余3条评论

20

基于@Travis Castillo的答案。修复了以下问题:

  • 移动整个布局导致视图顶部的对象消失。
  • 在相互之后立即显示 SnackBars 时,无法将布局向上推。

因此,这里是 MoveUpwardBehavior 类的修复代码:

import android.content.Context;
import android.support.annotation.Keep;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;

@Keep
public class MoveUpwardBehavior extends CoordinatorLayout.Behavior<View> {

    public MoveUpwardBehavior() {

        super();

    }

    public MoveUpwardBehavior(Context context, AttributeSet attrs) {

        super(context, attrs);

    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {

        return dependency instanceof Snackbar.SnackbarLayout;

    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        float translationY = Math.min(0, ViewCompat.getTranslationY(dependency) - dependency.getHeight());

        //Dismiss last SnackBar immediately to prevent from conflict when showing SnackBars immediately after eachother
        ViewCompat.animate(child).cancel();

        //Move entire child layout up that causes objects on top disappear
        ViewCompat.setTranslationY(child, translationY);

        //Set top padding to child layout to reappear missing objects
        //If you had set padding to child in xml, then you have to set them here by <child.getPaddingLeft(), ...>
        child.setPadding(0, -Math.round(translationY), 0, 0);

        return true;
    }

    @Override
    public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {

        //Reset paddings and translationY to its default
        child.setPadding(0, 0, 0, 0);
        ViewCompat.animate(child).translationY(0).start();

    }
}

当SnackBar显示时,此代码将向上推送用户在屏幕上看到的内容,并且用户可以访问布局中的所有对象。

如果您希望SnackBar覆盖对象而不是推送它们,并且用户确实可以访问所有对象,则需要更改onDependentViewChanged方法:

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    float translationY = Math.min(0, ViewCompat.getTranslationY(dependency) - dependency.getHeight());

    //Dismiss last SnackBar immediately to prevent from conflict when showing SnackBars immediately after eachother
    ViewCompat.animate(child).cancel();

    //Padding from bottom instead pushing top and padding from top.
    //If you had set padding to child in xml, then you have to set them here by <child.getPaddingLeft(), ...>
    child.setPadding(0, 0, 0, -Math.round(translationY));

    return true;
}

以及方法 onDependentViewRemoved

@Override
public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {

    //Reset paddings and translationY to its default
    child.setPadding(0, 0, 0, 0);

}

很遗憾,当用户滑动删除 SnackBar 时,您将失去动画效果。您需要使用 ValueAnimator 类来制作填充变化的动画,但这可能会导致一些冲突,您需要进行调试。

https://developer.android.com/reference/android/animation/ValueAnimator.html

欢迎就滑动删除 SnackBar 的动画提供任何评论。

如果您可以跳过该动画,那么您可以使用它。

无论如何,我推荐第一种方法。


这个答案非常好。我发现了一个情况,它的表现与我预期的不同。如果我嵌套 <CoordinatorLayout><FrameLayout app:layout_behavior="com.example.MoveUpwardBehavior"><ScrollView><LinearLayout>,那么当 SnackBar 将包含 FrameLayout 向上推动时,ScrollView 会产生尴尬的滚动。而使用 Travis Castello 的答案,它只会被推上去。 - TTransmit
对这个解决方案点赞。我试图在BottomSheetDialogFragment中显示Snackbar,但是其他的解决方案都不起作用。这个解决方案完美地解决了我的问题。谢谢! - 4gus71n

16

4

我实施了这个功能,发现当小贴士消失后,视图仍停留在原处,留下小贴士位置的白色空间。显然,如果设备上已禁用动画,则会出现此问题。

为了解决这个问题,我将onDependentViewChanged方法更改为存储附加此行为的视图的初始Y位置。然后,在删除小贴士时,将该视图的位置重置为存储的Y位置。

private static float initialPositionY;

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    initialPositionY = child.getY();
    float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
    child.setTranslationY(translationY);
    return true;
}

@Override
public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
    super.onDependentViewRemoved(parent, child, dependency);
    child.setTranslationY(initialPositionY);
}

Travis Castillo的回答在Snackbar显示时存在故障,而这个答案运行非常顺畅。 - Nishant Pardamwar

3
进一步改进Travis Castillo的答案。 这个修复了卡顿的动画 + 转换为Kotlin。
不需要手动进行Y轴平移,因为它会自动处理。 手动进行Y轴平移实际上会使动画看起来很卡顿。
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.annotation.Keep
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import com.google.android.material.snackbar.Snackbar.SnackbarLayout
import kotlin.math.min
import kotlin.math.roundToInt

/**
 * To use this, the parent container must be a CoordinatorLayout.
 * This can be applied to a child ViewGroup with app:layout_behavior="com.example.MoveUpwardBehavior"
 */
@Keep
class MoveUpwardBehavior(context: Context?, attrs: AttributeSet?) : CoordinatorLayout.Behavior<View>(context, attrs) {

    override fun layoutDependsOn(parent: CoordinatorLayout, targetView: View, snackBar: View): Boolean {
        return snackBar is SnackbarLayout
    }

    /**
     * @param parent - the parent container
     * @param targetView - the view that applies the layout_behavior
     * @param snackBar
     */
    override fun onDependentViewChanged(parent: CoordinatorLayout, targetView: View, snackBar: View): Boolean {
        val bottomPadding = min(0f, snackBar.translationY - snackBar.height).roundToInt()

        //Dismiss last SnackBar immediately to prevent from conflict when showing SnackBars immediately after each other
        ViewCompat.animate(targetView).cancel()

        //Set bottom padding so the target ViewGroup is not hidden
        targetView.setPadding(targetView.paddingLeft, targetView.paddingTop, targetView.paddingRight, -bottomPadding)

        return true
    }

    override fun onDependentViewRemoved(parent: CoordinatorLayout, targetView: View, snackBar: View) {
        //Reset padding to default value
        targetView.setPadding(targetView.paddingLeft, targetView.paddingTop, targetView.paddingRight, 0)
    }
}

1
我已经编写了一个库,可以添加额外的视图以与SnackProgressBar一起进行动画处理。它还包括progressBar和其他内容。请尝试使用https://github.com/tingyik90/snackprogressbar
假设您有以下视图需要进行动画处理。
View[] views = {view1, view2, view3};

在您的活动中创建一个SnackProgressBarManager实例,并包含要进行动画处理的视图。
SnackProgressBarManager snackProgressBarManager = new SnackProgressBarManager(rootView)
        .setViewsToMove(views)

当 SnackProgressBar 显示或消失时,这些视图将相应地进行动画处理。

1
除了Travis Castillo的回答之外: 为了允许触发连续的SnackBars,在onDependentViewChanged()中,您必须取消由onDependentViewRemoved()启动的任何可能正在进行的动画:
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
    float translationY = Math.min(0, ViewCompat.getTranslationY(dependency) - dependency.getHeight());
    ViewCompat.animate(child).cancel(); //cancel potential animation started in onDependentViewRemoved()
    ViewCompat.setTranslationY(child, translationY);
    return true;
}

没有取消的情况下,当一个 SnackBar 被另一个 SnackBar 替换时,LinearLayout 会跳到第二个 SnackBar 的下面。

0

@Markymark提出了一个很好的解决方案,但是在snackbar的第一帧中,translationY==0,在第二帧中,translationY==全高度并开始正确地减少,因此依赖布局在第一帧上会出现卡顿。这可以通过跳过第一帧+恢复原始填充作为良好的副作用来修复。

class MoveUpwardBehavior(context: Context?, attrs: AttributeSet?) : CoordinatorLayout.Behavior<View>(context, attrs), Parcelable {

    var originalPadding = -1

    constructor(parcel: Parcel) : this(
        TODO("context"),
        TODO("attrs")
    ) {
    }

    override fun layoutDependsOn(parent: CoordinatorLayout, targetView: View, snackBar: View): Boolean {
        return snackBar is Snackbar.SnackbarLayout
    }

    /**
     * @param parent - the parent container
     * @param targetView - the view that applies the layout_behavior
     * @param snackBar
     */
    override fun onDependentViewChanged(parent: CoordinatorLayout, targetView: View, snackBar: View): Boolean {

        if (originalPadding==-1) {
            originalPadding = targetView.paddingBottom
            return true
        }
        val bottomPadding = min(0f, snackBar.translationY - snackBar.height).roundToInt()
//        println("bottomPadding: ${snackBar.translationY} ${snackBar.height}")

        //Dismiss last SnackBar immediately to prevent from conflict when showing SnackBars immediately after each other
        ViewCompat.animate(targetView).cancel()

        //Set bottom padding so the target ViewGroup is not hidden
        targetView.setPadding(targetView.paddingLeft, targetView.paddingTop, targetView.paddingRight, -(bottomPadding-originalPadding))

        return true
    }

    override fun onDependentViewRemoved(parent: CoordinatorLayout, targetView: View, snackBar: View) {
        //Reset padding to default value
        targetView.setPadding(targetView.paddingLeft, targetView.paddingTop, targetView.paddingRight, originalPadding)
        originalPadding = -1
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {

    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<MoveUpwardBehavior> {
        override fun createFromParcel(parcel: Parcel): MoveUpwardBehavior {
            return MoveUpwardBehavior(parcel)
        }

        override fun newArray(size: Int): Array<MoveUpwardBehavior?> {
            return arrayOfNulls(size)
        }
    }
}

-4

不需要使用协调布局,只需使用常规视图将 Snackbar 对齐到视图底部,并将按钮放置在其上方,单击按钮后根据您的逻辑显示 Snackbar 或线性布局。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
/*snackbar code

 <LinearLayout
            android:id="@+id/linear_snack bar"
            android:layout_width="match_parent"
            android:layout_height="@dimen/margin_45"
            android:layout_alignParentBottom="true"
            android:layout_marginTop="@dimen/margin_0"
            android:background="@color/dark_grey">

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="@color/orange"
                android:gravity="center"
                android:textColor="@color/white"
                android:textSize="@dimen/text_size_h7" />

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:gravity="center"
                android:textSize="@dimen/text_size_h7" />
        </LinearLayout>

<View  android:layout_above="@+id/linear_snack bar"



</RelativeLayout>

2
我无法确定您是否以前使用过snackbar,但这不是常见的实现方式。通常,它是在运行时添加的动态警告。如果这是对您有用的技术,请尝试更具体地说明,而不是在“snackbar代码”中进行注释,因为这可能是布局中最重要的部分之一。 - Travis Castillo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接