Search code examples
androidxamarin.androidmvvmcross

RecyclerView scroll lags


I am writing an application for Android with Xamarin.Android and MvvmCross. But I think my question applies to native Android in general.

I wrote a custom RecyclerView so that I can have buttons on the side when I swipe on a row. It all works well, except when I scroll the list it lags a lot. The only thing that I can do to fix it is to remove everything in the xml of the row layout except for a single TextView, but I'm sure there's a way to fix the lag while keeping the contents of the row layout. Here is my (relevant) code:

public class SwipeMvxRecyclerView : MvxRecyclerView
{
    private SwipeMvxRecyclerStateHandler _stateHandler;

    private ICommand _leftButtonClick;
    private ICommand _rightButtonClick;
    private ICommand _mainButtonClick;

    public ICommand LeftButtonClick
    {
        get { return _leftButtonClick; }
        set
        {
            if (ReferenceEquals(_leftButtonClick, value))
            {
                return;
            }

            _leftButtonClick = value;
        }
    }

    public ICommand RightButtonClick
    {
        get { return _rightButtonClick; }
        set
        {
            if (ReferenceEquals(_rightButtonClick, value))
            {
                return;
            }

            _rightButtonClick = value;
        }
    }

    public ICommand MainButtonClick
    {
        get { return _mainButtonClick; }
        set
        {
            if (ReferenceEquals(_mainButtonClick, value))
            {
                return;
            }

            _mainButtonClick = value;
        }
    }

    public View SwipeView { get; set; }

    public SwipeMvxRecyclerView(Context context, IAttributeSet attr) : base(context, attr)
    {
        Initialize();
    }

    public SwipeMvxRecyclerView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
    {
        Initialize();
    }

    public override bool OnTouchEvent(MotionEvent e)
    {
        if (e.Action == MotionEventActions.Down)
        {
            var v = FindChildViewUnder(e.GetX(), e.GetY());
            SwipeView = v.FindViewById<View>(Resource.Id.swipe_view);
        }

        switch(e.Action)
        {
            case MotionEventActions.Down: _stateHandler.Down(SwipeView, e.GetX(), e.GetY()); break;
            case MotionEventActions.Move: _stateHandler.Move(e.GetX(), e.GetY()); break;
            case MotionEventActions.Up: _stateHandler.Up(e.GetX()); break;
        }

        if (_stateHandler.DisableScroll)
        {
            return true;
        }
        else
        {
            return base.OnTouchEvent(e);
        }
    }

    private void Initialize()
    {
        _stateHandler = new SwipeMvxRecyclerStateHandler(Context.Resources.GetDimension(Resource.Dimension.search_result_item_button_width));
        _stateHandler.MainItemClicked += OnMainButtonClicked;
        _stateHandler.LeftItemClicked += OnLeftButtonClicked;
        _stateHandler.RightItemClicked += OnRightButtonClicked;
    }

    private void OnMainButtonClicked(object sender, EventArgs e)
    {
        var viewHolder = FindContainingViewHolder(SwipeView);

        var item = Adapter.GetItem(viewHolder.LayoutPosition); // What different is viewHolder.AdapterPosition? I tested it with 100 items and it's always the same, but I'm not sure if this will never break...

        MainButtonClick.Execute(item);
    }

    private void OnLeftButtonClicked(object sender, EventArgs e)
    {
        var viewHolder = FindContainingViewHolder(SwipeView);

        var item = Adapter.GetItem(viewHolder.LayoutPosition); // What different is viewHolder.AdapterPosition? I tested it with 100 items and it's always the same, but I'm not sure if this will never break...

        LeftButtonClick.Execute(item);
    }

    private void OnRightButtonClicked(object sender, EventArgs e)
    {
        var viewHolder = FindContainingViewHolder(SwipeView);

        var item = Adapter.GetItem(viewHolder.LayoutPosition); // What different is viewHolder.AdapterPosition? I tested it with 100 items and it's always the same, but I'm not sure if this will never break...

        RightButtonClick.Execute(item);
    }
}

I wrote this class to handle the swiping/scrolling events on my custom Recycleview:

public class SwipeMvxRecyclerStateHandler
{
    private enum SwipeState
    {
        Idle = 0,
        Scrolling = 1,
        Swiping = 2
    }

    private enum SwipePosition
    {
        Center = 0,
        Left = 1,
        Right = 2
    }

    private SwipeState _currentSwipeState;
    private SwipePosition _currentSwipePosition;

    private View _swipeView;

    private bool _isClicking;
    private float _originalClickX;
    private float _originalClickY;
    private float _originalPositionX;

    private float _buttonWidth;
    private float _swipingThreshold = 20;
    private float _scrollingThreshold = 20;
    private int _snapAnimationDuration = 150;

    public event EventHandler MainItemClicked;
    public event EventHandler LeftItemClicked;
    public event EventHandler RightItemClicked;

    public bool DisableScroll { get { return _currentSwipeState == SwipeState.Swiping; }}

    public SwipeMvxRecyclerStateHandler(float buttonWidth)
    {
        _buttonWidth = buttonWidth;
    }

    public void Down(View swipeView, float x, float y)
    {
        _isClicking = true;
        var previousSwipeView = _swipeView;
        _swipeView = swipeView;

        // Close it if it's still open
        if (previousSwipeView != null && previousSwipeView != _swipeView)
        {
            previousSwipeView.Animate()
                             .X(0)
                             .SetDuration(_snapAnimationDuration)
                             .Start();

            _originalPositionX = 0;
        }

        _currentSwipeState = SwipeState.Idle;
        _originalClickX = x;
        _originalClickY = y;
    }

    public void Move(float x, float y)
    {
        _isClicking = false;

        switch(_currentSwipeState)
        {
            case SwipeState.Idle: MoveOnIdleState(x, y); break;
            case SwipeState.Swiping: MoveOnSwipingState(x); break;
            // Nothing to do on SwipeState.Scrolling
        }
    }

    public void Up(float x)
    {
        if (_currentSwipeState != SwipeState.Scrolling)
        {
            if (_isClicking)
            {
                FindAndExecuteClickedButton(x);
            }
            else
            {
                SnapToClosestPosition(x);
            }
        }

        _isClicking = false;
        _currentSwipeState = SwipeState.Idle;
    }

    private void FindAndExecuteClickedButton(float x)
    {
        // I am making the assumption that there are 2 buttons, one on the left, another on the right.
        // You will need to modify this code if you want to add more buttons.

        if(_currentSwipePosition == SwipePosition.Center)
        {
            MainItemClicked?.Invoke(null, EventArgs.Empty);
        }
        else if(_currentSwipePosition == SwipePosition.Left && x > _swipeView.Width - _buttonWidth && x < _swipeView.Width)
        {
            RightItemClicked?.Invoke(null, EventArgs.Empty);
        }
        else if(_currentSwipePosition == SwipePosition.Right && x > 0 && x < _buttonWidth)
        {
            LeftItemClicked?.Invoke(null, EventArgs.Empty);
        }
        else
        {
            _swipeView.Animate()
                  .X(0)
                  .SetDuration(_snapAnimationDuration)
                  .Start();

            _originalPositionX = 0;
        }
    }

    private void SnapToClosestPosition(float x)
    {
        float moveX = x - _originalClickX;
        float newPositionX = _originalPositionX + moveX;

        float distanceToShowLeftButton = Math.Abs(_buttonWidth - newPositionX);
        float distanceToShowRightButton = Math.Abs(-_buttonWidth - newPositionX);
        float distanceToCenter = Math.Abs(newPositionX);

        float positionToSnapTo;

        if (distanceToShowLeftButton > distanceToCenter)
        {
            if (distanceToShowRightButton > distanceToCenter)
            {
                positionToSnapTo = 0;
                _currentSwipePosition = SwipePosition.Center;
            }
            else
            {
                positionToSnapTo = -_buttonWidth;
                _currentSwipePosition = SwipePosition.Left;
            }
        }
        else if (distanceToShowRightButton > distanceToCenter)
        {
            positionToSnapTo = _buttonWidth;
            _currentSwipePosition = SwipePosition.Right;
        }
        else
        {
            positionToSnapTo = 0;
            _currentSwipePosition = SwipePosition.Center;
        }

        _swipeView.Animate()
                  .X(positionToSnapTo)
                  .SetDuration(_snapAnimationDuration)
                  .Start();

        _originalPositionX = positionToSnapTo;
    }

    private void MoveOnIdleState(float x, float y)
    {
        if (Math.Abs(x - _originalClickX) > _swipingThreshold)
        {
            _currentSwipeState = SwipeState.Swiping;
        }
        else if (Math.Abs(y - _originalClickY) > _scrollingThreshold)
        {
            _currentSwipeState = SwipeState.Scrolling;
        }
    }

    private void MoveOnSwipingState(float x)
    {
        float moveX = x - _originalClickX;
        float newPositionX = _originalPositionX + moveX;

        if(x < _originalClickX)
        {
            newPositionX += _swipingThreshold;
        }
        else
        {
            newPositionX -= _swipingThreshold;
        }

        if (newPositionX > _buttonWidth)
        {
            _swipeView.Animate()
                      .X(_buttonWidth)
                      .SetDuration(0)
                      .Start();
        }
        else if (newPositionX < -_buttonWidth)
        {
            _swipeView.Animate()
                      .X(-_buttonWidth)
                      .SetDuration(0)
                      .Start();
        }
        else
        {
            _swipeView.Animate()
                      .X(newPositionX)
                      .SetDuration(0)
                      .Start();
        }
    }
}

}

This is the xml for the row layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
    android:id="@+id/left_button"
    android:layout_width="@dimen/search_result_item_button_width"
    android:layout_height="@dimen/search_result_item_height"
    android:gravity="center"
    android:background="@color/add_to_wishlist_button_background">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="@dimen/text_huge"
            android:textColor="@color/white"
            local:MvxBind="Text IsAddedToWishlist, Converter=BoolToFontAwesome, ConverterParameter=fa-heart-o|fa-heart; Style ., Converter=String, ConverterParameter=fonts/fontawesome.ttf" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_medium"
            android:textSize="@dimen/text_tiny"
            android:textColor="@color/white"
            local:MvxBind="Style ., Converter=String, ConverterParameter=fonts/roboto/Roboto-Regular.ttf"
            local:MvxLang="Text add_to_wishlist" />
    </LinearLayout>
</RelativeLayout>
<RelativeLayout
    android:id="@+id/right_button"
    android:layout_width="@dimen/search_result_item_button_width"
    android:layout_height="@dimen/search_result_item_height"
    android:layout_alignParentRight="true"
    android:gravity="center"
    android:background="@color/add_to_cart_button_background">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="@dimen/text_huge"
            android:textColor="@color/white"
            local:MvxBind="Text IsAddedToCart, Converter=BoolToFontAwesome, ConverterParameter=fa-shopping-cart|fa-cart-plus; Style ., Converter=String, ConverterParameter=fonts/fontawesome.ttf" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margin_medium"
            android:textSize="@dimen/text_tiny"
            android:textColor="@color/white"
            local:MvxBind="Style ., Converter=String, ConverterParameter=fonts/roboto/Roboto-Regular.ttf"
            local:MvxLang="Text add_to_cart" />
    </LinearLayout>
</RelativeLayout>
<RelativeLayout
    android:id="@+id/swipe_view"
    android:layout_width="match_parent"
    android:layout_height="@dimen/search_result_item_height"
    android:background="@color/white">
    <FrameLayout
        android:id="@+id/thumbnail_container"
        android:layout_width="@dimen/search_result_item_thumbnail_container_width"
        android:layout_height="match_parent"
        android:padding="@dimen/padding_tiny">
        <Mvx.MvxImageView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:scaleType="fitXY"
            android:adjustViewBounds="true"
            local:MvxBind="ImageUrl ThumbnailUrl" />
    </FrameLayout>
    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/thumbnail_container"
        android:textSize="@dimen/text_medium"
        android:textColor="@color/black"
        local:MvxBind="Text Title; Style ., Converter=String, ConverterParameter=fonts/roboto/Roboto-Bold.ttf" />
    <TextView
        android:id="@+id/manufacturer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/thumbnail_container"
        android:layout_below="@id/title"
        android:layout_marginTop="@dimen/margin_tiny"
        android:textSize="@dimen/text_tiny"
        android:textColor="@color/text_gray"
        local:MvxBind="Text Manufacturer; Style ., Converter=String, ConverterParameter=fonts/roboto/Roboto-Regular.ttf" />
    <MyProject.Droid.Components.Rating
        android:id="@+id/rating"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/manufacturer"
        android:layout_toRightOf="@id/thumbnail_container"
        android:layout_marginTop="@dimen/margin_tiny"
        local:MvxBind="NumberOfStars NumberOfStars" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/rating"
        android:layout_toRightOf="@id/thumbnail_container"
        android:layout_marginTop="@dimen/margin_tiny"
        android:textSize="@dimen/text_small"
        android:textColor="@color/black"
        local:MvxBind="Text Price, Converter=MoneySign; Style ., Converter=String, ConverterParameter=fonts/roboto/Roboto-Bold.ttf" />
</RelativeLayout>
<View
    android:layout_width="match_parent"
    android:layout_height="@dimen/horizontal_line_height"
    android:layout_alignParentBottom="true"
    android:background="@color/horizontal_line" />

It still lags even if I remove the Mvx.MvxImageView. Do I simply have too many things in my row layout?


Solution

  • I realized that it's lagging because it's loading the .ttf file every time it binds when scrolling through the list. Loading it just once solved the problem.