Как стать автором
Обновить

Реализация одновременного скролинга по всем направлениям

Время на прочтение 6 мин
Количество просмотров 5.4K

Введение


Доброго времени суток! Сразу скажу, что программировать я начал недавно и большого опыта у меня нет, поэтому не судите строго, тем более, что материалов на данную тему очень мало. В статье я хочу поделиться своим решением проблемы, которая у меня возникла при создании пошаговой 2D стратегии. Для стратегий привычное дело наличие игрового поля. Но как быть, если у пользователя маленький телефон и всё игровое поле не помещается на экране? Таким вопросом я задался примерно месяц назад, когда у меня ещё ничего не было готово. Сначала я решил как обычно обернуть поле в ScrollView и HorizontalScrollView. И тут начинается собственно проблема. Прокручивать можно было только по одному направлению одновременно, что очень неудобно, тем более для игры. Если вам интересно решение этой проблемы добро пожаловать под кат.

Разбираемся


Для начала нужно подумать, что у нас уже есть, а не пытаться сразу изобретать что-то своё. Поискав некоторое время в Google документации информацию по классу ScrollView становится ясно, что не всё так плохо. Посмотрим на метод onInterceptTouchEvent(MotionEvent ev). Он вызывается для определения перехвата касаний — это то, что нам нужно. Надо сделать так, чтобы он ловил все движения, кроме нажатий на элемент поля. Вот как это выглядит:

Класс вертикальной прокрутки
public final class MultiScrollView extends ScrollView {
    //Координаты текущего касания
    private int origX, origY;
    //Если палец сдвинулся на величину больше, чем 60 px, то происходит скроллинг
    private final float THRESHOLD = 60;

    public MultiScrollView(Context context) {
        super(context);
    }

    public MultiScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MultiScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean canScrollHorizontally(int direction) {
        return true;
    }

    @Override
    public boolean canScrollVertically(int direction) {
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            origX = (int) ev.getX();
            origY = (int) ev.getY();
        } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            float deltaX = Math.abs(ev.getX() - origX);
            float deltaY = Math.abs(ev.getY() - origY);
            return deltaX >= THRESHOLD || deltaY >= THRESHOLD;
        }
        return false;
    }
}


Как вы видите касания перехватываются только если изменение координаты пальца больше 60 px. Так же в этом классе необходимо переопределить метод canScrollHorizontally, чтобы он всегда возвращал true.

Так же нужно создать свой класс горизонтальной прокрутки:

Класс горизонтальной прокрутки
public final class MyHorizontalScrollView extends HorizontalScrollView {
    public MyHorizontalScrollView(Context context) {
        super(context);
    }

    public MyHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

    @Override
    public boolean canScrollHorizontally(int direction) {
        return false;
    }

  @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
}


Тут мы запрещаем ему реагировать на какие — либо касания.

Проверяем


Теперь необходимо проверить как это работает. Создадим разметку:

Разметка активности - activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <example.multiscroll.MultiScrollView
        android:id="@+id/field_y"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:background="@color/color_black"
        android:fadingEdgeLength="0dp"
        android:focusable="true"
        android:isScrollContainer="true"
        android:overScrollMode="never"
        android:requiresFadingEdge="none"
        android:scrollbars="none"
        android:splitMotionEvents="true"
        tools:context="example.multiscroll.MainActivity">

        <example.multiscroll.MyHorizontalScrollView
            android:id="@+id/field_x"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fadingEdgeLength="0dp"
            android:focusable="false"
            android:isScrollContainer="true"
            android:overScrollMode="never"
            android:requiresFadingEdge="none"
            android:scrollbars="none"
            android:splitMotionEvents="true">

            <TableLayout
                android:id="@+id/table"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@color/color_white"
                android:orientation="vertical"
                android:paddingBottom="70dp"
                android:paddingEnd="140dp"
                android:paddingLeft="140dp"
                android:paddingRight="140dp"
                android:paddingStart="140dp"
                android:paddingTop="70dp" />
        </example.multiscroll.MyHorizontalScrollView>
    </example.multiscroll.MultiScrollView>
</RelativeLayout>


Разметка элемента поля - sq.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:layout_margin="5dp">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>


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

Код активности - MainActivity.java
public class MainActivity extends Activity {
    private ScrollView scrollY;
    private GestureDetector gestureDetectorY;
    private RelativeLayout[][] sqContent;
    private int sizeI = 10;
    private int sizeJ = 20;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sqContent = new RelativeLayout[sizeI][sizeJ];
        scrollY = (MultiScrollView) findViewById(R.id.field_y);
        gestureDetectorY = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                scrollX.smoothScrollBy((int) distanceX, 0);
                scrollY.smoothScrollBy(0, (int) (distanceY));
                return true;
            }
        });
        scrollY.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                gestureDetectorY.onTouchEvent(event);
                return true;
            }
        });
        TableLayout table = (TableLayout) findViewById(R.id.table);
        TableRow row[] = new TableRow[sizeJ];
        TableRow.LayoutParams paramsTableRow = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT);
        paramsTableRow.setMargins(0, 50, 0, 50);
        paramsTableRow.weight = 1;
        for (int i = 0; i < sizeI; i++) {
            row[i] = new TableRow(this);
            row[i].setLayoutParams(paramsTableRow);
            row[i].setWeightSum(sizeJ);
            for (int j = 0; j < sizeJ; j++) {
                sqContent[i][j] = (RelativeLayout) getLayoutInflater().inflate(R.layout.sq, row[i], false);
                sqContent[i][j].setBackgroundResource(R.color.color_sq_action);
                sqContent[i][j].setOnClickListener(getOnClickListener());
                row[i].addView(sqContent[i][j], j);
            }
            table.addView(row[i], i);
        }
    }

    private View.OnClickListener getOnClickListener() {
        return new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                v.setBackgroundResource(R.color.color_black);
            }
        };
    }
}


Для примера я взял размеры поля 20 на 10. Особое внимание тут следует уделить объекту класса GestureDetector. Он нужен для определения жестов. В обработчике касаний для горизонтальной и вертикальной прокрутки мы передаём ему событие. Он в свою очередь вызывает скролл по оси X и Y. Так же для проверки удобства нажатий на элементы поля я повесил на них слушатель, который меняет их цвет. Даже если не нажимать, а немного провести(<= 60 px), то цвет поменяется. Таким образом, проблем с удобством нажатий не возникает. Надеюсь я достаточно подробно объяснил своё решение и оно вам стало понятно.

Исходники этого проекта вы можете скачать тут.
Теги:
Хабы:
+1
Комментарии 1
Комментарии Комментарии 1

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн