RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 672702
Accepted
RareScrap
RareScrap
Asked:2020-05-30 15:54:24 +0000 UTC2020-05-30 15:54:24 +0000 UTC 2020-05-30 15:54:24 +0000 UTC

ItemTouchHelper 中的自定义动画

  • 772

我使用ItemTouchHelper“滑动删除”来实现该功能。我知道还有其他方法可以做到这一点,但我读到使用ItemTouchHelper是最方便和现代的,此外它不需要第三方库。

这就是我的应用程序目前的行为方式。一旦屏幕上没有剩余元素,图片就会显示在中间

在此处输入图像描述

购物车在ShoppingCartFragment.javaRecyclerView片段中实现,此片段中使用的适配器在ShoppingCartItemRecyclerViewAdapter.java中实现。抱歉,我不是在这里发布代码,而是在 github 上发布——这是由于 30,000 个字符的限制。我将我的整个项目放在StackOverflow 的一个单独的分支中,这样这段代码就不会在未来消失。

问题

  1. 为什么滑动时元素的顶部和底部有边缘?

    在此处输入图像描述

  2. 为什么在删除列表中间的元素后留下的红色背景高度整齐地降低(gif 1),而在删除元素后,如果这个元素是列表中唯一的一个,动画不会改变高度列表(gif 2)。

    动态图 1 动态图 2

  3. 如何在 UNDO 按钮出现的动画过程中去除黑色阴影(即当她从左边驶出时)?在我看来,我需要更改隐藏在 ItemTouchHelper 某处的撤消按钮外观的标准动画,但我不知道在哪里做(即哪个方法对此负责)

    在此处输入图像描述

  4. 最重要的问题- 如何为位于滑动移动的元素“后面”的对象设置自己的动画?例如,Gmail 使用完全不同的动画来显示 UNDO 按钮:

    在此处输入图像描述

我将很高兴收到任何意见、链接和建议。如果我不清楚某处,请随时提问。

android
  • 1 1 个回答
  • 10 Views

1 个回答

  • Voted
  1. Best Answer
    RareScrap
    2020-07-22T09:30:42Z2020-07-22T09:30:42Z

    这个答案稍后会更新。

    因此,这就是我设法找到的有关上述问题的信息。别忘了我链接到我github上相应分支的项目代码。

    1)在我看来,有两个可能的原因:它与用于 RecyclerView 的 ItemDecorator(ShoppingCartFragment.java,第 344 行)有关,或者我们看不到 RecyclerView 项目的影子,因为 它们之间没有距离,阴影只是“隐藏”在相邻的列表项下。我现在没有时间或机会找出答案,所以这个问题仍然可以被认为是开放的。一旦我弄清楚阴影的这种行为是什么,我会立即更新答案。

    2)这是因为你看到的“动画”是一种错觉。事实上,红色背景本身并没有改变高度——它只是与下面的元素重叠。如果您在标记中添加缩进,这很容易看出。

    动画速度减慢 10 倍

    为了消除这种悲惨的行为,将红色背景的顶部和底部边缘分别锚定到顶部和底部列表项就足够了。

        /**
         * Подготавливает объект {@link ItemTouchHelper} и его колбэк
         * {@link android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback} для прослушивания
         * свайпов и реализации основных графических эфектов свайпов (например, таких как красный фон
         * позади элемента, сдвигаемого свайпом)
         */
        private void setUpItemTouchHelper() {
            ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
                /** Фон, который показывается "за" элеметом при свайпе */
                Drawable background;
                /** Рисунок, информирующей пользователя, что сделаый иим свайп приведет к удалеию элемета */
                Drawable deleteMark;
                /** Отступы {@link #deleteMark}'а */
                int xMarkMargin;
                /** Флаг того, что метод {@link #init()} был вызван */
                boolean initiated;
    
                /** Инициализирует ресурсы графики, требуемые для свайпа. Например, {@link #background} */
                private void init() {
                    background = new ColorDrawable(Color.RED);
                    deleteMark = ContextCompat.getDrawable(getActivity(), R.drawable.ic_delete_24dp); // Получение ресурса "мусорной корзины"
                    deleteMark.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP); // Фильтр, которй делает корзину белой
                    xMarkMargin = (int) getActivity().getResources().getDimension(R.dimen.swipe_delete_mark_margin);
                    initiated = true;
                }
    
                /**
                 * Метод, обязательный для реализации, но не использующийся в данном приложении.
                 * Нужен для реализации "drag & drop".
                 * @param recyclerView RecyclerView, к которому прикрепляется нащ ItemTouchHelper
                 * @param viewHolder ViewHolder (т.е. элемент списка), который подвергается перетаскиванию
                 *                   по инициативе пользователя
                 * @param target ViewHolder, над которым перетаскивается текущий активный элемент
                 * @return True, если viewHolder перемещен в позицию адаптера элемета target.
                 */
                @Override
                public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
                    return false;
                }
    
                /**
                 * Возвращает допустимые направления свайпа для данного объекта viewHolder.
                 * Эти допустимые направления свайпов настраиваются либо в кострукторе, либо в {@link #setDefaultSwipeDirs(int)}.
                 * @param recyclerView {@link RecyclerView}, к которому прикрепляется наш {@link ItemTouchHelper}
                 * @param viewHolder {@link RecyclerView.ViewHolder}, для которого запашивается направление свайпа
                 * @return Логическое сложение допустимых свайпов (допустимый свайп представляет собой одну из костан группы "Direction Flag")
                 */
                @Override
                public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
                    // Порядковый номер элемента, по которому пользователь хочет сделать свайп
                    int position = viewHolder.getAdapterPosition();
    
                    // Запретить свайп, если появился красный фон, ожидающий нажатие кнопки Undo
                    if (rvAdapter.undoOn && rvAdapter.isPendingRemoval(position)) {
                        return 0;
                    }
                    return super.getSwipeDirs(recyclerView, viewHolder);
                }
    
                /**
                 * Вызывается когда свайп доведен до конца и палец пользователя убран с экрана.
                 * Помещает в очередь на удаление или удаляет элемет списка в зависимости от
                 * значения флага {@link ShoppingCartItemRecyclerViewAdapter#undoOn}.
                 * @param viewHolder
                 * @param swipeDir
                 */
                @Override
                public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
                    // Порядковый номер элемента, по которому был сделан свайп
                    int swipedPosition = viewHolder.getAdapterPosition();
                    if (rvAdapter.undoOn) {
                        rvAdapter.pendingRemoval(swipedPosition); // Добавить элемент в список элементов, ожидающих удаление
                    } else {
                        rvAdapter.remove(swipedPosition); // Просто удалить элемент
                    }
                }
    
                /**
                 * Вызывается для отрисовки всех объектов позади элемента списка, который сдвинули свайпом.
                 * Подробнее:
                 * <a href="https://developer.android.com/reference/android/support/v7/widget/helper/ItemTouchHelper.Callback.html#onChildDraw(android.graphics.Canvas,%20android.support.v7.widget.RecyclerView,%20android.support.v7.widget.RecyclerView.ViewHolder,%20float,%20float,%20int,%20boolean)"></a>
                 *
                 * <p>
                 * Этот метод так же вызывается, когда элемент сдвинут, палец убран, но список плавно
                 * "задвигает" сдвинутый элемент. При этом viewHolder.getAdapterPosition() дает -1.
                 * </p>
                 *
                 * @param c {@link Canvas}, на котором RecyclerView рисует то, что ему скажут
                 * @param recyclerView {@link RecyclerView}, к которому прикрепляется наш {@link ItemTouchHelper}
                 * @param viewHolder {@link RecyclerView.ViewHolder}, который анимируется из-за действий пользователя, или сам по себе
                 * @param dX Величина горизонтального смещения (отностительно "нормальной" позиции элемента), вызванного действием пользователя
                 * @param dY Величина вертикального смещения (отностительно "нормальной" позиции элемента), вызванного действием пользователя
                 * @param actionState Тип взаимодействия во View. Это может быть ACTION_STATE_DRAG или ACTION_STATE_SWIPE.
                 * @param isCurrentlyActive True, когда анимация вызвана дейсвтиями юзера и False, когда анимация работает "сама по себе"
                 */
                @Override
                public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
                    // View, по которому был сделан свайп
                    View itemView = viewHolder.itemView;
                    int pos = viewHolder.getLayoutPosition(); // TODO: getAdapterPosition()
    
                    // Этот if сработает, когда элемент сдвинут, палец убран, но список плавно "задвигает" сдвинутый элемент
                    if (viewHolder.getAdapterPosition() == -1) {
                        // not interested in those
                        return;
                    }
    
                    // Проверка на то, были ли инииализированы графические ресурсы для рисования фона и прочих объектов
                    if (!initiated) {
                        init();
                    }
    
                    // Вьюха предыдущего элемента (null, если ее абсолютно нет на экране)
                    View previousView = recyclerView.getLayoutManager().findViewByPosition(pos-1);
                    // Вьюха следующего элемента (null, если ее абсолютно нет на экране)
                    View nextView = recyclerView.getLayoutManager().findViewByPosition(pos+1);
    
                    // Получение координат вьюх элементов списка
                    int[] nextViewLocation = new int[2]; // Координаты следующей (за свайпнутой) вьюхи
                    int[] itemViewLocation = new int[2];  // Координаты свайпнутой вьюхи
                    int[] previousViewLocation = new int[2];  // Координаты предыдущей вьюхи
                    ViewGroup.MarginLayoutParams lp1 = null; // Инициализация отступов для следующей ...
                    ViewGroup.MarginLayoutParams lp2 = null; // ... и предыдущей вьюхи
    
                    if (itemView != null) // Если элемент есть на экране
                        itemView.getLocationInWindow(itemViewLocation); // TODO: Как будет вести себя этот кусок кода, если анимации в процессе, а вьюхи нет на экране. Этого можно досич если во время анимации пролистать список вниз
                    if (previousView != null) { // Если предыдущей элемент есть на экране (хотя бы частично)
                        previousView.getLocationInWindow(previousViewLocation); // Получить координаты предыдущего элемента
                        lp2 = (ViewGroup.MarginLayoutParams) previousView.getLayoutParams(); // Получить отступы предыдущего элемента
                    }
                    if (nextView != null) { // Если следущющий элемент есть на экране (хотя бы частично)
                        nextView.getLocationInWindow(nextViewLocation); // Получить координаты следующего элемента
                        lp1 = (ViewGroup.MarginLayoutParams) nextView.getLayoutParams(); // Получить отступы следующего элемента
                    }
    
                    // Вычисления координат высот элеметов
                    int cordTop;
                    int cordBottom;
                    ActionBar ab = ((MainActivity) getActivity()).getSupportActionBar();
    
                    // Вычисление высоты статус бара и action бара
                    // TODO: Не зннаю как будет работать для полноэкранного режима. Имеет смысл добавить проверку в if ниже
                    int resultStatusBar = 0;
                    int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
                    if (resourceId > 0) {
                        resultStatusBar = getResources().getDimensionPixelSize(resourceId);
                    }
                    int barsOffset = resultStatusBar+ab.getHeight(); // Смещение для канвы по оси Y
    
                    if (previousView != null) {
                        cordTop = previousViewLocation[1]+previousView.getHeight()- barsOffset+lp2.topMargin+lp2.bottomMargin;
                        //Log.d( String.valueOf( previousView.getTop() ), String.valueOf( cordTop ));
                    } else {
                        cordTop = itemView.getTop();
                    }
    
                    if (nextView != null) {
                        //Log.d( String.valueOf( nextView.getTop() ), String.valueOf( nextViewLocation[1] ));
                        //Log.d( "a", "1");
                        cordBottom = nextViewLocation[1] - barsOffset-lp1.topMargin-lp1.bottomMargin;
                    } else {
                        //Log.d( "a", "2");
                        cordBottom = itemView.getBottom(); // TODO: Учитывает ли getBottom отступы?
                    }
    
                    background.setBounds(itemView.getRight() + (int) dX, cordTop, itemView.getRight(), cordBottom);
                    background.draw(c);
    
                    // Рисование иконки удаления
                    // Замер элемета, по которому был сдела свайп
                    int itemHeight = itemView.getBottom() - itemView.getTop(); // Растояние в пикселях!
                    // Замер размеов иконки удаления
                    int intrinsicWidth = deleteMark.getIntrinsicWidth(); // Слово "Intrinsic" можно просто отбросить
                    int intrinsicHeight = deleteMark.getIntrinsicWidth();
    
                    // Определение позиции икоки на красном фоне
                    int xMarkLeft = itemView.getRight() - xMarkMargin - intrinsicWidth;
                    int xMarkRight = itemView.getRight() - xMarkMargin;
                    int xMarkTop = itemView.getTop() + (itemHeight - intrinsicHeight)/2;
                    int xMarkBottom = xMarkTop + intrinsicHeight;
    
                    // Определяет размеры квадратой области за сдвинутым элеметом, где будет нарисован xMark
                    deleteMark.setBounds(xMarkLeft, xMarkTop, xMarkRight, xMarkBottom);
                    deleteMark.draw(c); // Рисует иконку удаления
    
                    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
                }
            };
            ItemTouchHelper mItemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
            mItemTouchHelper.attachToRecyclerView(recyclerView);
        }
    

    请记住,使用 getTop() 和 getBottom() 获取红色背景坐标是不可能的(至少它对我不起作用)。所以我使用 getLocationInWindow() 来做到这一点

    3) 要回答这个问题,您需要了解您在滑动之前和之后在屏幕上看到的视图实际上是同一个视图。此视图同时包含两种状态的元素,但默认情况下只有一种状态可见 - 这是滑动前视图中固有的状态。当滑动发生时,我们以编程方式隐藏“预滑动”状态的嵌套视图,并显示滑动后应该出现的视图(例如,撤消按钮)。这可以在第 157 行的文件ShoppingCartItemRecyclerViewAdapter.java中找到。但是最好在图片中显示列表项的内部而不是用文字解释(shopping_cart_item.xml: 为了摆脱阴影 - 我只是将它们关闭在代码中:撤消按钮被隐藏是因为  只有当列表项的所有其他视图都具有可见性时才会显示 GONE 滑动前用户看到的列表项目

        /**
         * Задает значения полям, хранящие ссылки на элементы GUI (например, для {@link #recyclerView})
         * @param view UI, которое вернул метод {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}
         * @param savedInstanceState Если не равно NULL, то фрагмент восстановился из предыдущего
         *                           сохраненного состояния. Этот объект  и есть его предыдущее состояние.
         */
        @Override
        public void onViewCreated (View view, Bundle savedInstanceState) {
    
            //...
    
            recyclerView.setItemAnimator(new DefaultItemAnimator() {
                /**
                 * Отвечает за анимацию "выдвижения" кнопки Undo (и, возможно, за что-то еще)
                 * @param oldHolder Элемент списка, который подвергся изменению
                 * @param newHolder Элемент списка, который получился из-за произошедших изменений
                 * @param fromX Левый край oldHolder
                 * @param fromY Вверхий край oldHolder
                 * @param toX Левый край newHolder
                 * @param toY Вверхий край newHolder
                 * @return True, если позже ожидается (запрашивается) вызов {@link #runPendingAnimations()}, иначе - false
                 */
                @Override
                public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
                                             int fromX, int fromY, int toX, int toY) {
                    // Получение ссылок на элемет CardView в oldHolder и newHolder
                    CardView oldHolderCardView = (CardView) oldHolder.itemView.findViewById(R.id.root_card_view);
                    CardView newHolderCardView = (CardView) newHolder.itemView.findViewById(R.id.root_card_view);
    
                    // Сохранение старых значение elevation (на свякий случай)
                    // СОХРАНЕНИЕ СТАРЫХ ЗНАЧЕНИЙ И ПОСЛЕДУЮЩЕЕ ИХ ИСПОЛЬЗОВАНИЕ ВЕДЕТ ТОМУ, ЧТО ТЕНЬ НЕ БУДЕТ УБИРАТЬСЯ!
                    //float oldHolderElevation = oldHolderCardView.getCardElevation();
                    //float newHolderElevation = newHolderCardView.getCardElevation();
    
                    // Убираем тень при выдвижении кнопки Undo
                    oldHolderCardView.setCardElevation(0);
                    newHolderCardView.setCardElevation(0);
    
                    // Запускаем стандартную анимацию
                    boolean returnedBool = super.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY);
    
                    // Возвращаем стандартных elevation
                    //oldHolderCardView.setCardElevation(oldHolderElevation);
                    //newHolderCardView.setCardElevation(newHolderElevation);
    
                    // Возвращем результат стандартной анимации
                    return returnedBool;
                }
            });
    

    4)我还没有这个问题的答案。我什至不知道去哪里挖。我希望稍后再回到这个问题并确保更新这个答案。

    • 3

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    Python 3.6 - 安装 MySQL (Windows)

    • 1 个回答
  • Marko Smith

    C++ 编写程序“计算单个岛屿”。填充一个二维数组 12x12 0 和 1

    • 2 个回答
  • Marko Smith

    返回指针的函数

    • 1 个回答
  • Marko Smith

    我使用 django 管理面板添加图像,但它没有显示

    • 1 个回答
  • Marko Smith

    这些条目是什么意思,它们的完整等效项是什么样的

    • 2 个回答
  • Marko Smith

    浏览器仍然缓存文件数据

    • 1 个回答
  • Marko Smith

    在 Excel VBA 中激活工作表的问题

    • 3 个回答
  • Marko Smith

    为什么内置类型中包含复数而小数不包含?

    • 2 个回答
  • Marko Smith

    获得唯一途径

    • 3 个回答
  • Marko Smith

    告诉我一个像幻灯片一样创建滚动的库

    • 1 个回答
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Алексей Шиманский 如何以及通过什么方式来查找 Javascript 代码中的错误? 2020-08-03 00:21:37 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5