Введение
Доброго времени суток! Сразу скажу, что программировать я начал недавно и большого опыта у меня нет, поэтому не судите строго, тем более, что материалов на данную тему очень мало. В статье я хочу поделиться своим решением проблемы, которая у меня возникла при создании пошаговой 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;
}
}
Тут мы запрещаем ему реагировать на какие — либо касания.
Проверяем
Теперь необходимо проверить как это работает. Создадим разметку:
<?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>
<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 текстовых поля, но для примеру это лишнее).
public class MainActivity extends Activity {
private ScrollView scrollY;
private HorizontalScrollView scrollX;
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);
scrollX = (MyHorizontalScrollView) findViewById(R.id.field_x);
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;
}
});
scrollX.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), то цвет поменяется. Таким образом, проблем с удобством нажатий не возникает. Надеюсь я достаточно подробно объяснил своё решение и оно вам стало понятно.
Исходники этого проекта вы можете скачать тут.
Заключение
В заключении мне хочется рассказать о моём проекте. Это пошаговая фэнтази стратегия. Игра на данный момент имеет 3 режима игры: Компания (захват вражеских замков под управлением ИИ), Игра 1 на 1 ( с одного телефона играют 2 игрока, которые выбирают себе юнитов сами(возможен контр — пик)) и Арена (один игрок отражает волны ботов). Мне хочется сделать его клиент — серверным, над чем я и веду сейчас работу. В команде я один, но был бы не против взять в команду художника. Если вам интересно такое предложение, то напишите мне в личку.
Вот как выглядит игра на данный момент, для того чтобы стала до конца понятна моя идея.
Автор: Константин Уренев