Скользящее изображение с официальной библиотекой поддержки 23.x. + bottomSheet, как google maps

Обновление
Я хочу выполнить то же поведение, что карты google имеют с поддержкой библиотеки 23.x. + и без ЛЮБОЙ 3-й библиотеки

ПРИМЕЧАНИЕ: это не дублированный вопрос, потому что:

  • Я хочу использовать Behaviors, Support Library и без ЛЮБОЙ сторонней библиотеки (я добавил ее под заголовком вопроса и выше)
  • Я хотел ВСЕ поведение, которое вы видите в следующем gif, другие вопросы задают одно или два поведения и используют ЛЮБОЙ ПУТЬ для его достижения.

    как вы можете видеть в этом gif

У меня уже работает официальный нижний лист (даже внутри вкладки и пейджера просмотра).

Что заставляет меня сходить с ума - как добиться поведения изображения, которое возникает из BottomSheet при продвижении по официальному нижнему листу?.

Я пробовал использовать якорь, как FAB, без успеха.
Я читал что-то об использовании прослушивателя прокрутки, но ppl сказал, что он не плавный и быстрый, как карты google.

Мой XML (я не думаю, что он поможет, но в любом случае):

<?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=".ui.MasterActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay"
            app:layout_scrollFlags="scroll|enterAlways|snap">

            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                style="?android:attr/borderlessButtonStyle"
                android:text="Departure"
                android:layout_gravity="center"
                android:id="@+id/buttonToolBar"
                />


        </android.support.v7.widget.Toolbar>

        <android.support.design.widget.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabBackground="@android:color/white"
            app:tabTextColor="@color/colorAccent"
            app:tabSelectedTextColor="@color/colorAccent"/>

    </android.support.design.widget.AppBarLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />


    <android.support.v4.widget.NestedScrollView
        android:id="@+id/asdf"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:behavior_peekHeight="100dp"
        android:fitsSystemWindows="true"
            app:layout_behavior="android.support.design.widget.BottomSheetBehavior">

        <LinearLayout
            android:id="@+id/qwert"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:paddingBottom="16dp"
            android:background="@android:color/white"
            android:padding="15dp">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="BOOTOMSHEET TITLE"
                    android:textAppearance="@style/TextAppearance.AppCompat.Title" />

            <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Button1"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="text 2"
                android:layout_margin="10dp"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="text 3"
                android:layout_margin="10dp"/>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="text 4"
                android:layout_margin="10dp"/>


            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="320dp"
                android:background="@color/colorAccent">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="Your remaining content here"
                    android:textColor="@android:color/white" />

            </FrameLayout>
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>


    <android.support.design.widget.FloatingActionButton
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        app:layout_anchor="@id/asdf"
        app:layout_anchorGravity="top|right|end"
        android:src="@drawable/abc_ic_search_api_mtrl_alpha_copy"
        android:layout_margin="@dimen/fab_margin"
        android:clickable="true"/>

</android.support.design.widget.CoordinatorLayout>

Ответ 1

Если вы хотите достичь этого с помощью Support Library 23.4.0. + Я расскажу вам, как я получил его и как его работы.

Примечание: извиниться за мой английский, я попытался дать ответ на программирование (просто короткий и смешанный с полезным кодом), но похоже, что это было недостаточно хорошо...

Насколько я могу видеть, что активность/фрагмент имеет следующие поведения:

  • 2 панели инструментов с анимациями, которые реагируют на движение нижнего листа.
  • FAB, который скрывается, когда он находится рядом с "модальной панелью инструментов" (тот, который появляется, когда вы приближаетесь).
  • Изображение заднего плана за нижним листом с каким-то эффектом параллакса.
  • Заголовок (TextView) на панели инструментов, который появляется, когда нижний лист достигает его.
  • Панель состояния уведомлений может превратить свой фон в прозрачный или полный цвет.
  • Пользовательское поведение нижнего листа с состоянием "привязки".

note2: В этом ответе поговорите о 6 вещах не о 1 или 2, как о другом вопросе, вы можете увидеть разницу сейчас?

Хорошо, теперь можно проверить один за другим:

Панели инструментов
Когда вы открываете это представление в картах google, вы можете видеть панель инструментов, в которой вы можете искать, это единственная, что я не делаю, как карты Google, потому что я хотел сделать это более общим. В любом случае ToolBar находится внутри AppBarLayout, и он скрыт, когда вы начинаете перетаскивать нижний лист и снова появляется, когда BottomSheet достигает состояния COLLAPSED.
Для этого вам нужно:

  • создать Behavior и расширить его от AppBarLayout.ScrollingViewBehavior
  • переопределить методы layoutDependsOn и onDependentViewChanged. Сделав это, вы будете слушать движения нижнего листа.
  • создайте некоторые методы, чтобы скрыть и отобразить AppBarLayout/ToolBar с анимацией.

Так я сделал это для первой панели инструментов или ActionBar:

@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 (mChild == null) {
        initValues(child, dependency);
        return false;
    }

    float dVerticalScroll = dependency.getY() - mPreviousY;
    mPreviousY = dependency.getY();

    //going up
    if (dVerticalScroll <= 0 && !hidden) {
        dismissAppBar(child);
        return true;
    }

    return false;
}

private void initValues(final View child, View dependency) {

    mChild = child;
    mInitialY = child.getY();

    BottomSheetBehaviorGoogleMapsLike bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(dependency);
    bottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, @BottomSheetBehaviorGoogleMapsLike.State int newState) {
            if (newState == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED ||
                    newState == BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN)
                showAppBar(child);
        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {

        }
    });
}

private void dismissAppBar(View child){
    hidden = true;
    AppBarLayout appBarLayout = (AppBarLayout)child;
    mToolbarAnimation = appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_shortAnimTime));
    mToolbarAnimation.y(-(mChild.getHeight()+25)).start();
}

private void showAppBar(View child) {
    hidden = false;
    AppBarLayout appBarLayout = (AppBarLayout)child;
    mToolbarAnimation = appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_mediumAnimTime));
    mToolbarAnimation.y(mInitialY).start();
}

полный файл, если вам это нужно

Вторая панель инструментов или "Модальная" панель инструментов:
Вы должны переопределить те же методы, но в этом вам нужно позаботиться о более поведении:

  • показать/скрыть панель инструментов с анимацией
  • изменить цвет статуя/цвет фона
  • показать/скрыть заголовок BottomSheet в ToolBar
  • закрыть нижний лист или отправить его в сложенное состояние.

Код для этого немного обширен, поэтому я дам ссылку

FAB

Это также пользовательское поведение, но оно простирается от FloatingActionButton.Behavior. В onDependentViewChanged вам нужно посмотреть, когда он достигнет "offSet" или указать, где вы хотите скрыть его. В моем случае я хочу скрыть его, когда он находится рядом со второй панелью инструментов, поэтому я копаюсь в родительском блоке FAB (файл CoordiantorLayout), который ищет AppBarLayout, который содержит ToolBar, затем я использую позицию ToolBar, например OffSet:

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

    if (offset == 0)
        setOffsetValue(parent);

    if (dependency.getY() <=0)
        return false;

    if (child.getY() <= (offset + child.getHeight()) && child.getVisibility() == View.VISIBLE)
        child.hide();
    else if (child.getY() > offset && child.getVisibility() != View.VISIBLE)
        child.show();

    return false;
}

Полная пользовательская ссылка на поведение FAB

Изображение за нижним листом с эффектом параллакса:
Как и другие, это обычное поведение, единственная "сложная" вещь в этом - это маленький алгоритм, который удерживает изображение привязанным к BottomSheet и избегает обрушения изображения, как эффект параллакса по умолчанию:

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

    if (mYmultiplier == 0) {
        initValues(child, dependency);
        return true;
    }

    float dVerticalScroll = dependency.getY() - mPreviousY;
    mPreviousY = dependency.getY();

    //going up
    if (dVerticalScroll <= 0 && child.getY() <= 0) {
        child.setY(0);
        return true;
    }

    //going down
    if (dVerticalScroll >= 0 && dependency.getY() <= mImageHeight)
        return false;

    child.setY( (int)(child.getY() + (dVerticalScroll * mYmultiplier) ) );

    return true;
}


полный файл для фона Изображение с эффектом параллакса

Теперь для завершения: Пользовательское поведение нижнего листа
Для достижения 3-х шагов сначала вам нужно понять, что Default BottomSheetBehavior по умолчанию имеет 5 состояний: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN и для поведения в Google Картах вам нужно добавить среднее состояние между свернутым и расширенным: STATE_ANCHOR_POINT.
Я пробовал расширять дефолт bottomSheetBehavior без успеха, поэтому я просто скопировал весь код и модифицировал то, что мне нужно.
Чтобы достичь того, о чем я говорю, следуйте следующим шагам:

  • Создайте класс Java и продолжите его с CoordinatorLayout.Behavior<V>
  • Скопируйте код копии из файла BottomSheetBehavior по умолчанию в новый.
  • Измените метод clampViewPositionVertical следующим кодом:

    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        return constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
    }
    int constrain(int amount, int low, int high) {
        return amount < low ? low : (amount > high ? high : amount);
    }
    
  • Добавить новое состояние

    public static final int STATE_ANCHOR_POINT = X;

  • Измените следующие методы: onLayoutChild, onStopNestedScroll, BottomSheetBehavior<V> from(V view) и setState (необязательно)



public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
    // First let the parent lay it out
    if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {
        if (ViewCompat.getFitsSystemWindows(parent) &&
                !ViewCompat.getFitsSystemWindows(child)) {
            ViewCompat.setFitsSystemWindows(child, true);
        }
        parent.onLayoutChild(child, layoutDirection);
    }
    // Offset the bottom sheet
    mParentHeight = parent.getHeight();
    mMinOffset = Math.max(0, mParentHeight - child.getHeight());
    mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);

    //if (mState == STATE_EXPANDED) {
    //    ViewCompat.offsetTopAndBottom(child, mMinOffset);
    //} else if (mHideable && mState == STATE_HIDDEN...
    if (mState == STATE_ANCHOR_POINT) {
        ViewCompat.offsetTopAndBottom(child, mAnchorPoint);
    } else if (mState == STATE_EXPANDED) {
        ViewCompat.offsetTopAndBottom(child, mMinOffset);
    } else if (mHideable && mState == STATE_HIDDEN) {
        ViewCompat.offsetTopAndBottom(child, mParentHeight);
    } else if (mState == STATE_COLLAPSED) {
        ViewCompat.offsetTopAndBottom(child, mMaxOffset);
    }
    if (mViewDragHelper == null) {
        mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
    }
    mViewRef = new WeakReference<>(child);
    mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
    return true;
}


public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
    if (child.getTop() == mMinOffset) {
        setStateInternal(STATE_EXPANDED);
        return;
    }
    if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) {
        return;
    }
    int top;
    int targetState;
    if (mLastNestedScrollDy > 0) {
        //top = mMinOffset;
        //targetState = STATE_EXPANDED;
        int currentTop = child.getTop();
        if (currentTop > mAnchorPoint) {
            top = mAnchorPoint;
            targetState = STATE_ANCHOR_POINT;
        }
        else {
            top = mMinOffset;
            targetState = STATE_EXPANDED;
        }
    } else if (mHideable && shouldHide(child, getYVelocity())) {
        top = mParentHeight;
        targetState = STATE_HIDDEN;
    } else if (mLastNestedScrollDy == 0) {
        int currentTop = child.getTop();
        if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
            top = mMinOffset;
            targetState = STATE_EXPANDED;
        } else {
            top = mMaxOffset;
            targetState = STATE_COLLAPSED;
        }
    } else {
        //top = mMaxOffset;
        //targetState = STATE_COLLAPSED;
        int currentTop = child.getTop();
        if (currentTop > mAnchorPoint) {
            top = mMaxOffset;
            targetState = STATE_COLLAPSED;
        }
        else {
            top = mAnchorPoint;
            targetState = STATE_ANCHOR_POINT;
        }
    }
    if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
        setStateInternal(STATE_SETTLING);
        ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
    } else {
        setStateInternal(targetState);
    }
    mNestedScrolled = false;
}

public final void setState(@State int state) {
    if (state == mState) {
        return;
    }
    if (mViewRef == null) {
        // The view is not laid out yet; modify mState and let onLayoutChild handle it later
        /**
         * New behavior (added: state == STATE_ANCHOR_POINT ||)
         */
        if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
                state == STATE_ANCHOR_POINT ||
                (mHideable && state == STATE_HIDDEN)) {
            mState = state;
        }
        return;
    }
    V child = mViewRef.get();
    if (child == null) {
        return;
    }
    int top;
    if (state == STATE_COLLAPSED) {
        top = mMaxOffset;
    } else if (state == STATE_ANCHOR_POINT) {
        top = mAnchorPoint;
    } else if (state == STATE_EXPANDED) {
        top = mMinOffset;
    } else if (mHideable && state == STATE_HIDDEN) {
        top = mParentHeight;
    } else {
        throw new IllegalArgumentException("Illegal state argument: " + state);
    }
    setStateInternal(STATE_SETTLING);
    if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
        ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
    }
}


public static <V extends View> BottomSheetBehaviorGoogleMapsLike<V> from(V view) {
    ViewGroup.LayoutParams params = view.getLayoutParams();
    if (!(params instanceof CoordinatorLayout.LayoutParams)) {
        throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
    }
    CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
            .getBehavior();
    if (!(behavior instanceof BottomSheetBehaviorGoogleMapsLike)) {
        throw new IllegalArgumentException(
                "The view is not associated with BottomSheetBehaviorGoogleMapsLike");
    }
    return (BottomSheetBehaviorGoogleMapsLike<V>) behavior;
}



ссылка на проект дыр, где вы можете увидеть все пользовательские поведения

note3: в следующий раз добавьте комментарий, просящий вежливым способом для изменения ответа или спросите, почему этот ответ имеет НЕКОТОРЫЕ равные вещи, чем другие мои ответы по той же теме. ДО ТОГО, как закрыть его или пометить как дублирующийся.

И вот как это выглядит:
[CustomBottomSheetBehavior]

Ответ 2

Вы можете добиться эффекта, используя поведение макета координатора. Вам нужно будет расширить класс CoordinatorLayout.Behaviour и написать зависимость над одним из представлений в макете координатора, сохранив изображение, содержащее представление в качестве дочернего элемента. Простым образом вам нужно прикрепить пользовательское письменное поведение к изображению, содержащему представление, Для получения помощи при написании пользовательского поведения, пожалуйста, перейдите по ссылке Написание пользовательских действий