Поиск по этому блогу

воскресенье, 18 января 2015 г.

Изменение позиции элемента в списке Android

Я долго искал как сделать список с айтемами которые можно перетаскивать между собой, нашел кучу примеров которые работали но то не сохраняли положение куда переместили или например был пример который непонятно было как интегрировать в код, это библиотека github.com/bauerca/drag-sort-listview, возможно у меня руки кривые но я не смог ее поставить так что бы она работала, вечно что то вылетало и падало… В общем не подходило мне ничего, и я взялся говнокодить тот код который уже нашел до этого, который работал не верно.

В общем я вам сейчас открою страшную тайну как всегда (:

Для начала мы создадим кастомный ListView, создайте класс DragListView. Сейчас туда мы впишем много непонятного кода который творит чудеса. А ниже я объясню как он работает (частично ибо в некоторых местах я сам не понимаю этой магии).

DragListView.java

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;

public class DragListView extends ListView {

    private ImageView dragImageView;
    private int dragSrcPosition;
    private int dragPosition;

    private int dragPoint;
    private int dragOffset;

    private WindowManager windowManager;
    private WindowManager.LayoutParams windowParams;

    private int scaledTouchSlop;
    private int upScrollBounce;
    private int downScrollBounce;

    public DragListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // захыватываем наш палец который коснулся экрана, и следим за ним
        if(ev.getAction()==MotionEvent.ACTION_DOWN){
            //запоминаем координаты пальца
            int x = (int)ev.getX();
            int y = (int)ev.getY();

            // переводим их в точки на экране, и проверяем не вылезли ли мы за пределы
            dragSrcPosition = dragPosition = pointToPosition(x, y);
            if(dragPosition== AdapterView.INVALID_POSITION){
                return super.onInterceptTouchEvent(ev);
            }

            //берем ту позицию на которую мы надавили и берем этот айтем и отслеживаем перемещения пальца или чем вы там тыкаете в экран
            ViewGroup itemView = (ViewGroup) getChildAt(dragPosition-getFirstVisiblePosition());
            dragPoint = y - itemView.getTop();
            dragOffset = (int) (ev.getRawY() - y);

            //магия
            View dragger = itemView.findViewById(R.id.radioButton);
            if(dragger!=null&&x>dragger.getLeft()-20){
                upScrollBounce = Math.min(y-scaledTouchSlop, getHeight()/3);
                downScrollBounce = Math.max(y+scaledTouchSlop, getHeight()*2/3);

                itemView.setDrawingCacheEnabled(true);
                Bitmap bm = Bitmap.createBitmap(itemView.getDrawingCache());
                startDrag(bm, y);
            }
            return false;
        }
        return super.onInterceptTouchEvent(ev);
    }

    /**
     *
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if(dragImageView!=null&&dragPosition!=INVALID_POSITION){
            int action = ev.getAction();
            switch(action){
                case MotionEvent.ACTION_UP:
                    int upY = (int)ev.getY();
                    stopDrag();
                    onDrop(upY);
                    break;
                case MotionEvent.ACTION_MOVE:
                    int moveY = (int)ev.getY();
                    onDrag(moveY);
                    break;
                default:break;
            }
            return true;
        }
        //
        return super.onTouchEvent(ev);
    }

    /**
     * Метод обрабатывающий перетаскивание айтемов 
     * @param bm
     * @param y
     */
    public void startDrag(Bitmap bm ,int y){
        stopDrag();

        //из кода меняем позици елемента который мы схватили на координаты которые мы получили в onInterceptTouchEvent()
        windowParams = new WindowManager.LayoutParams();
        windowParams.gravity = Gravity.TOP;
        windowParams.x = 0;
        windowParams.y = y - dragPoint + dragOffset;
        windowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        windowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        windowParams.format = PixelFormat.TRANSLUCENT;
        windowParams.windowAnimations = 0;

        ImageView imageView = new ImageView(getContext());
        imageView.setImageBitmap(bm);
        windowManager = (WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE);
        windowManager.addView(imageView, windowParams);
        dragImageView = imageView;
    }

    /**
     * удаляем лишний вью и вставляем его ниже
     */
    public void stopDrag(){
        if(dragImageView!=null){
            windowManager.removeView(dragImageView);
            dragImageView = null;
        }
    }

    /**
     * перемещаем по экрану айтемы
     * снова куча магии
     * @param y
     */
    public void onDrag(int y){
        if(dragImageView!=null){
            windowParams.alpha = 0.8f;
            windowParams.y = y - dragPoint + dragOffset;
            windowManager.updateViewLayout(dragImageView, windowParams);
        }
        int tempPosition = pointToPosition(0, y);
        if(tempPosition!=INVALID_POSITION){
            dragPosition = tempPosition;
        }

        int scrollHeight = 0;
        if(y<upScrollBounce){
            scrollHeight = 8;
        }else if(y>downScrollBounce){
            scrollHeight = -8;
        }

        if(scrollHeight!=0){
            setSelectionFromTop(dragPosition, getChildAt(dragPosition-getFirstVisiblePosition()).getTop()+scrollHeight);
        }
    }

    /**
     * что же делать чкогда бросили? верно сохраняем позицию если она валидна
     * куча магии
     * @param y
     */
    public void onDrop(int y){
        //здесь мы замещаем айтем который на данный момент находится
        // на своей позиции вниз что бы вставить тот что тащили и удаляем тот что был вместо него
        // и ставим его ниже
        int tempPosition = pointToPosition(0, y);
        if(tempPosition!=INVALID_POSITION){
            dragPosition = tempPosition;
        }
        if(y<getChildAt(1).getTop()){
            dragPosition = 1;
        }else if(y>getChildAt(getChildCount()-1).getBottom()){
            dragPosition = getAdapter().getCount()-1;
        }

        //
        if(dragPosition>0&&dragPosition<getAdapter().getCount()){
            DragListAdapter adapter = (DragListAdapter)getAdapter();
            String dragItem = adapter.getItem(dragSrcPosition);
            adapter.remove(dragItem);
            adapter.insert(dragItem, dragPosition);
        }

    }
}

Если в кратце то в этом классе мы полностью переопределяем методы которые отвечают за ту или иную работу ListView, в нашем случае это оказались методы onInterceptTouchEvent() и onTouchEvent() который отвечают за передачу координат косаний на экран.

В этом классе мы их переопределяем и записываем координаты места где и когда было обработано нажатие на тот или иной элемент. Дальше мы создаем метод (startDrag(),onDrag(), stopDrag() и onDrop()) которые работают на магии, я пытался как то разобраться но все что я понял я изложил в комментариях к коду. В них мы обрабатываем координаты нажатий которые мы получили из методов onInterceptTouchEvent() и onTouchEvent() и делаем ту или иную задачу отвечающую данному методу. 

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

Ладно переходим дальше, теперь нам надо в нашей activity_main.xml вписать наш кастомный ListView, иначе оно не будет работать на обычном, не зря ж мы создавали этот сложнейший класс DragListView (:

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <dajver.draggblelist.DragListView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/listView"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />
</RelativeLayout>

Все очень просто, даже пояснять мне кажется не нужно, просто вставили наш кастомный класс.

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

Сперва создадим кастомный леяут для отображения айтемов. (Ужас как закрутил, простите меня) Назовем его custom_item.

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

    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="60dp"
        android:layout_margin="5dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:text="Large Text"
            android:id="@+id/name"
            android:layout_marginLeft="34dp"
            android:layout_centerVertical="true"
            android:layout_toRightOf="@+id/radioButton"
            android:layout_toEndOf="@+id/radioButton" />

        <RadioButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/radioButton"
            android:enabled="true"
            android:checked="false"
            android:clickable="false"
            android:layout_centerVertical="true"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true" />
    </RelativeLayout>
</LinearLayout>

Тут у нас будет TextView для отображения собственно самого текста, и радиобаттон, просто для красоты) Что бы не тупо текст и было видно что это кастом, короче выпендриваюсь я.

А вот так будет выглядеть сам адаптер

DragListAdapter.java
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.List;

public class DragListAdapter extends ArrayAdapter<String> {

    private Context mContext;
    public DragListAdapter(Context context, List<String> objects) {
        super(context, 0, objects);
        this.mContext = context;
    }

    @Override
    public boolean isEnabled(int position) {
        return super.isEnabled(position);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view = convertView;
        view = LayoutInflater.from(getContext()).inflate(R.layout.custom_item, null);

        TextView textView = (TextView)view.findViewById(R.id.name);
        textView.setText(getItem(position));

        return view;
    }
}

Здесь ничего супер особенного нет, просто кастомный адаптер с переопределенным TextView для отображения текста, и все… Думаю если вам надо что то еще просто добавьте его на леяут и инициализируйте его в этом классе как я это сделал с TextView.

Ну а теперь нам осталось самое главное, в уже созданном ранее классе MainActivity, нам нужно инициализировать адаптер, список, и заполнить его разной чушью)

MainActivity.java
import android.app.Activity;
import android.os.Bundle;

import java.util.ArrayList;

public class MainActivity extends Activity {

    private ArrayList<String> list;
    private DragListAdapter dragListAdapter = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //просто заполняем массив что бы список не был пуст
        list = new ArrayList<String>();
        for(int i=1; i<10; i++){
            list.add("Item " + i);
        }

        //инициализируем лист вью
        DragListView dragListView = (DragListView) findViewById(R.id.listView);
        //заполняем адаптер
        dragListAdapter = new DragListAdapter(this, list);
        //выводим в листвью
        dragListView.setAdapter(dragListAdapter);
    }
}


Вот и все, теперь вам осталось скомпилировать данный код и у вас должно получиться что то похожее на это:

Когда только скомпилировали


После того как наперетаскивали все добро как попало :)


Надеюсь что понятно объяснил все, так как я объяснял так как я понимаю сам, буду надеяться что доступно, хотя в статье больше трепа чем объяснений) Всем спасибо за внимание кто дочитал аж доседа.

Исходники
GitHub && DropBox