Dodawanie kolorowego tła z tekstem/ikoną pod przesuniętym wierszem podczas korzystania z RecyclerView Androida

EDIT: prawdziwym problemem było to, że mój LinearLayout został zawinięty w inny układ, co spowodowało nieprawidłowe zachowanie. Przyjęta przez Sanvywell odpowiedź ma lepszy, pełniejszy przykład jak narysować kolor w widoku swiped niż fragment kodu, który podałem w pytaniu.

Teraz, gdy RecyclerView widget ma natywną obsługę przesuwania wierszy za pomocą klasy ItemTouchHelper , próbuję użyć go w aplikacji, w której wiersze będą zachowuj się podobnie do aplikacji Inbox Google. Oznacza to, że przesunięcie w lewo wykonuje jedną akcję, a przesunięcie w prawo wykonuje inną.

Implementacja samych działań była łatwa przy użyciu ItemTouchHelper.SimpleCallback ' S onSwiped metoda. Jednak nie byłem w stanie znaleźć prosty sposób, aby ustawić kolor i ikonę, która powinna pojawić się w widoku, który jest obecnie przesuwane (jak w aplikacji Google Inbox).

Aby to zrobić, próbuję obejść ItemTouchHelper.SimpleCallback ' S onChildDraw metoda jak ta:

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView,
                        RecyclerView.ViewHolder viewHolder, float dX, float dY,
                        int actionState, boolean isCurrentlyActive) {
    RecyclerViewAdapter.ViewHolder vh = (RecyclerViewAdapter.ViewHolder) viewHolder;
    LinearLayout ll = vh.linearLayout;

    Paint p = new Paint();
    if(dX > 0) {
        p.setARGB(255, 255, 0, 0);
    } else {
        p.setARGB(255, 0, 255, 0);
    }

    c.drawRect(ll.getLeft(), ll.getTop(), ll.getRight(), ll.getBottom(), p);

    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}

Określenie kierunku przesunięcia z dX i ustawienie odpowiedniego koloru działa zgodnie z przeznaczeniem, ale współrzędne, które otrzymuję z ViewHolder zawsze odpowiadają miejscu, w którym pierwsza LinearLayout została napompowana.

Jak uzyskać poprawne współrzędne LinearLayout, Które znajduje się w aktualnie przesuwanym wierszu? Czy istnieje łatwiejszy sposób (który nie wymaga nadpisywania onChildDraw), aby ustawić kolor tła i ikonę?

Author: Manvis, 2015-06-13

7 answers

Starałem się również zaimplementować tę funkcję, ale ty skierowałeś mnie we właściwym kierunku.

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
        // Get RecyclerView item from the ViewHolder
        View itemView = viewHolder.itemView;

        Paint p = new Paint();
        if (dX > 0) {
            /* Set your color for positive displacement */

            // Draw Rect with varying right side, equal to displacement dX
            c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
                    (float) itemView.getBottom(), p);
        } else {
            /* Set your color for negative displacement */

            // Draw Rect with varying left side, equal to the item's right side plus negative displacement dX
            c.drawRect((float) itemView.getRight() + dX, (float) itemView.getTop(),
                    (float) itemView.getRight(), (float) itemView.getBottom(), p);
        }

        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }
}
 54
Author: Sanvywell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-03 21:14:02

Przyjęta odpowiedź świetnie sprawdza się w kolorowaniu tła, ale nie odnosi się do rysowania ikony.

To działało dla mnie, ponieważ zarówno ustawić kolor tła i narysował ikonę, bez ikona jest rozciągnięta podczas przesuwania, lub pozostawiając odstęp między poprzednimi i kolejnymi elementami po przesunięciu.

public static final float ALPHA_FULL = 1.0f;

public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
    if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
        // Get RecyclerView item from the ViewHolder
        View itemView = viewHolder.itemView;

        Paint p = new Paint();
        Bitmap icon;

        if (dX > 0) {
            /* Note, ApplicationManager is a helper class I created 
               myself to get a context outside an Activity class - 
               feel free to use your own method */

            icon = BitmapFactory.decodeResource(
                    ApplicationManager.getContext().getResources(), R.drawable.myleftdrawable);

            /* Set your color for positive displacement */
            p.setARGB(255, 255, 0, 0);

            // Draw Rect with varying right side, equal to displacement dX
            c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
                    (float) itemView.getBottom(), p);

            // Set the image icon for Right swipe
            c.drawBitmap(icon,
                    (float) itemView.getLeft() + convertDpToPx(16),
                    (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2,
                    p);
        } else {
            icon = BitmapFactory.decodeResource(
                    ApplicationManager.getContext().getResources(), R.drawable.myrightdrawable);

            /* Set your color for negative displacement */
            p.setARGB(255, 0, 255, 0);

            // Draw Rect with varying left side, equal to the item's right side
            // plus negative displacement dX
            c.drawRect((float) itemView.getRight() + dX, (float) itemView.getTop(),
                    (float) itemView.getRight(), (float) itemView.getBottom(), p);

            //Set the image icon for Left swipe
            c.drawBitmap(icon,
                    (float) itemView.getRight() - convertDpToPx(16) - icon.getWidth(),
                    (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2,
                    p);
        }

        // Fade out the view as it is swiped out of the parent's bounds
        final float alpha = ALPHA_FULL - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
        viewHolder.itemView.setAlpha(alpha);
        viewHolder.itemView.setTranslationX(dX);

    } else {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
    }
}

private int convertDpToPx(int dp){
    return Math.round(dp * (getResources().getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT));
}
 32
Author: HappyKatz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-03 21:45:48

Nie jestem pewien, jak te rozwiązania (przez @ Sanvywell, @ HappyKatz i @user2410066) działają dla Was, ale w moim przypadku potrzebowałem kolejnego sprawdzenia w metodzie onChildDraw.

Wygląda na to, że ItemTouchHelper zachowuje ViewHolders usuniętych wierszy na wypadek, gdyby trzeba było je przywrócić. Jest również wywołanie onChildDraw dla tych VHs oprócz VH jest przesunięty. Nie jestem pewien, co do konsekwencji zarządzania pamięcią tego zachowania, ale potrzebowałem dodatkowego sprawdzenia na początku onChildDraw, aby uniknąć rysowania dla " fantom" rzędy.

if (viewHolder.getAdapterPosition() == -1) {
    return;
}

CZĘŚĆ BONUSOWA:

Chciałem również kontynuować rysowanie, gdy inne wiersze animują się do nowych pozycji po usunięciu wiersza i nie mogłem tego zrobić w obrębie ItemTouchHelper i onChildDraw. W końcu musiałem dodać kolejny dekorator przedmiotów, aby to zrobić. To idzie w ten sposób:

public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (parent.getItemAnimator().isRunning()) {
        // find first child with translationY > 0
        // draw from it's top to translationY whatever you want

        int top = 0;
        int bottom = 0;

        int childCount = parent.getLayoutManager().getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getLayoutManager().getChildAt(i);
            if (child.getTranslationY() != 0) {
                top = child.getTop();
                bottom = top + (int) child.getTranslationY();                    
                break;
            }
        }

        // draw whatever you want

        super.onDraw(c, parent, state);
    }
}

Aktualizacja: napisałem post na blogu o recycler view swipe, aby usunąć funkcję. Komuś może się przydać. 3rd party lib nie jest konieczne.

Blog post git repo

 7
Author: Nemanja Kovacevic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-10-05 09:56:19

Oto Jak to zrobić bez 3rd party library.

Widok pierwszoplanowy będzie zawsze widoczny w widoku recyclera i po wykonaniu przesunięcia tło będzie widoczne w pozycja statyczna.

Tutaj wpisz opis obrazka

Utwórz niestandardowy element RecyclerView i dodaj niestandardową ikonę, tekst i kolor tła do układu tła elementu. Zauważ, że dodałem id do RelativeLayout z id=foreground i id=background.

Oto moje recylerview_item.xml.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">

    <RelativeLayout
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"> <!--Add your background color here-->

        <ImageView
            android:id="@+id/delete_icon"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginRight="10dp"
            app:srcCompat="@drawable/ic_delete"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginRight="10dp"
            android:layout_toLeftOf="@id/delete_icon"
            android:text="Swipe to delete"
            android:textColor="#fff"
            android:textSize="13dp" />
    </RelativeLayout>

    <RelativeLayout
        android:padding="20dp"
        android:id="@+id/foreground"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorWhite">

            <TextView
                android:id="@+id/textView"
                android:text="HelloWorld"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />

    </RelativeLayout>
</FrameLayout>

I z twojego ViewHolder zdefiniuj swoje RelativeLayout foreground i background view i upublicznij je. Utwórz również metodę, która usunie element. W moim przypadku mój Wizjer jest pod moim RecyclerViewAdapter.class, więc...

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {

    List<Object> listItem;

    public RecyclerViewAdapter(...) {
        ...
    } 

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = mInflater.inflate(R.layout.recyclerview_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        ....
    }

    @Override
    public int getItemCount() {
        ...
    }

    public class ViewHolder extends RecyclerView.ViewHolder{

        public RelativeLayout foreground, background;

        public ViewHolder(View itemView) {
            super(itemView);

            /** define your foreground and background **/

            foreground = itemView.findViewById(R.id.foreground);
            background = itemView.findViewById(R.id.background);

        }

    }

    /**Call this later to remove the item on swipe**/
    public void removeItem(int position){
        //remove the item here
        listItem.remove(position);
        notifyItemRemoved(position);
    }
}

I utworzyć klasę i nazwać ją RecyclerItemTouchHelper.class, to jest miejsce, w którym coś się stanie.

public class RecyclerItemTouchHelper extends ItemTouchHelper.SimpleCallback {

    private RecyclerItemTouchHelperListener listener;

    public RecyclerItemTouchHelper(int dragDirs, int swipeDirs, RecyclerItemTouchHelperListener listener) {
        super(dragDirs, swipeDirs);
        this.listener = listener;
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        return true;
    }

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        if (viewHolder != null) {
            final View foregroundView = ((RecyclerViewAdapter.ViewHolder) viewHolder).foreground;
            getDefaultUIUtil().onSelected(foregroundView);
        }
    }

    @Override
    public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
                                RecyclerView.ViewHolder viewHolder, float dX, float dY,
                                int actionState, boolean isCurrentlyActive) {
        final View foregroundView = ((RecyclerViewAdapter.ViewHolder) viewHolder).foreground;
        getDefaultUIUtil().onDrawOver(c, recyclerView, foregroundView, dX, dY,
                actionState, isCurrentlyActive);
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        final View foregroundView = ((RecyclerViewAdapter.ViewHolder) viewHolder).foreground;
        getDefaultUIUtil().clearView(foregroundView);
    }

    @Override
    public void onChildDraw(Canvas c, RecyclerView recyclerView,
                            RecyclerView.ViewHolder viewHolder, float dX, float dY,
                            int actionState, boolean isCurrentlyActive) {
        final View foregroundView = ((RecyclerViewAdapter.ViewHolder) viewHolder).foreground;

        getDefaultUIUtil().onDraw(c, recyclerView, foregroundView, dX, dY,
                actionState, isCurrentlyActive);
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        listener.onSwiped(viewHolder, direction, viewHolder.getAdapterPosition());
    }

    @Override
    public int convertToAbsoluteDirection(int flags, int layoutDirection) {
        return super.convertToAbsoluteDirection(flags, layoutDirection);
    }

    public interface RecyclerItemTouchHelperListener {
        void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position);
    }
}
Teraz, z twojego MainActivity.class lub gdziekolwiek jest twoje RecyclerView, dołącz do niego RecyclerItemTouchHelper. W moim przypadku RecyclerView jest w MainActivity.class, więc zaimplementowałem do niego RecyclerItemTouchHelper.RecyclerItemTouchHelperListener i nadpisałem metodę onSwiped()...
public class MainActivity extends AppCompatActivity implements RecyclerItemTouchHelper.RecyclerItemTouchHelperListener {

    RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        //Configure RecyclerView

        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);  
        RecyclerView.LayoutManager mLyoutManager = new LinearLayoutManager(getApplicationContext());
        recyclerView.setLayoutManager(mLyoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        adapter = new RecyclerViewAdapter(this);
        adapter.setClickListener(this);
        recyclerView.setAdapter(adapter);
        recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL));

        //Attached the ItemTouchHelper
        ItemTouchHelper.SimpleCallback itemTouchHelperCallback = new RecyclerItemTouchHelper(0, ItemTouchHelper.LEFT, this);
        new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerView);
    }

    //define the method onSwiped()
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, int position) {
        if (viewHolder instanceof RecyclerViewAdapter.ViewHolder) {
            adapter.removeItem(viewHolder.getAdapterPosition()); //remove the item from the adapter
        }
    }

}

Aby uzyskać więcej informacji i wyjaśnień tutaj jest blog dla niego.

 7
Author: Polar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-07-20 10:45:25

Rozwiązanie HappyKatz ma skomplikowany błąd. Czy jest jakiś powód do rysowania bitmapy przy dX= = 0?? W niektórych przypadkach powoduje to stałą widoczność ikon powyżej pozycji listy. Również ikony stają się widoczne powyżej pozycji listy, gdy tylko dotkniesz pozycji listy i dX==1. Aby je naprawić:

        if (dX > rectOffset) {
            c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
                    (float) itemView.getBottom(), leftPaint);
            if (dX > iconOffset) {
                c.drawBitmap(leftBitmap,
                        (float) itemView.getLeft() + padding,
                        (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - leftBitmap.getHeight()) / 2,
                        leftPaint);
            }
        } else if (dX < -rectOffset) {
            c.drawRect((float) itemView.getRight() + dX, (float) itemView.getTop(),
                    (float) itemView.getRight(), (float) itemView.getBottom(), rightPaint);
            if (dX < -iconOffset) {
                c.drawBitmap(rightBitmap,
                        (float) itemView.getRight() - padding - rightBitmap.getWidth(),
                        (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - rightBitmap.getHeight()) / 2,
                        rightPaint);
            }
        }
 6
Author: user2410066,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-05 13:37:05

Dla osób, które wciąż znajdują tę wartość domyślną, jest to najprostszy sposób.

Prosta klasa użytkowa do dodania tła, ikony i etykiety do elementu RecyclerView podczas przesuwania go w lewo lub w prawo.

Tutaj wpisz opis obrazka Tutaj wpisz opis obrazka

Insert to Gradle

implementation 'it.xabaras.android:recyclerview-swipedecorator:1.1'

Override onchilddraw metoda klasy ItemTouchHelper

@Override
public void onChildDraw (Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,float dX, float dY,int actionState, boolean isCurrentlyActive){
    new RecyclerViewSwipeDecorator.Builder(MainActivity.this, c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
            .addBackgroundColor(ContextCompat.getColor(MainActivity.this, R.color.my_background))
            .addActionIcon(R.drawable.my_icon)
            .create()
            .decorate();

    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}

Więcej informacji - > https://github.com/xabaras/RecyclerViewSwipeDecorator

 6
Author: kelvin andre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-07-07 20:54:07

Do implementacji wykorzystałem przykładowy kod stworzony przez Marcina Kitowicza tutaj.

Zalety tego rozwiązania:

  1. używa widoku tła z obramowaniami układu zamiast tworzenia prostokąta, który będzie wyświetlany na dowolnej Bitmapie lub Rysowalnej.
  2. używa Rysowalnego obrazu w przeciwieństwie do bitmapy, która jest łatwiejsza do zaimplementowania niż konieczność konwersji Rysowalnego na bitmapę.

Oryginalny kod implementacji można znaleźć tutaj . W celu zaimplementuj przesunięcie w lewo użyłem odwrotnej logiki pozycjonowania w lewo i w prawo.

override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
    var icon = ContextCompat.getDrawable(context!!, R.drawable.ic_save_24dp)
    var iconLeft = 0
    var iconRight = 0

    val background: ColorDrawable
    val itemView = viewHolder.itemView
    val margin = convertDpToPx(32)
    val iconWidth = icon!!.intrinsicWidth
    val iconHeight = icon.intrinsicHeight
    val cellHeight = itemView.bottom - itemView.top
    val iconTop = itemView.top + (cellHeight - iconHeight) / 2
    val iconBottom = iconTop + iconHeight

    // Right swipe.
    if (dX > 0) {
        icon = ContextCompat.getDrawable(context!!, R.drawable.ic_save_24dp)
        background = ColorDrawable(Color.RED)
        background.setBounds(0, itemView.getTop(), (itemView.getLeft() + dX).toInt(), itemView.getBottom())
        iconLeft = margin
        iconRight = margin + iconWidth
    } /*Left swipe.*/ else {
        icon = ContextCompat.getDrawable(context!!, R.drawable.ic_save_24dp)
        background = ColorDrawable(Color.BLUE)
        background.setBounds((itemView.right - dX).toInt(), itemView.getTop(), 0, itemView.getBottom())
        iconLeft = itemView.right - margin - iconWidth
        iconRight = itemView.right - margin
    }
    background.draw(c)
    icon?.setBounds(iconLeft, iconTop, iconRight, iconBottom)
    icon?.draw(c)
    super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
 3
Author: Adam Hurwitz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-31 01:10:18