View со свободным скроллингом
от Naik
Часто при создании игр требуется игровое поле, карта, или что-нибудь другое, что будет скролиться во всех направлениях. Для этого в разметке нужный View можно обернуть в контейнер ScrollView и одновременно HorizontalScrollView.
Но при таком подходе скроллинг будет работать только в одном направлении, в зависимости от направления жеста (вверх-вниз или вправо-влево).
В данной статье приведен пример готового виджета-заготовки, который скролится нормально в любом направлении, содержит скроллбары и поддерживает жест "бросок".
Для поддержки скроллбаров в папке проекта /res/values/ создайте файл attrs.xml со следующим содержанием:
И напоследок привожу пример использования класса:
Обновление
В Android Lolipop метод initializeScrollbars удален. Если удалить вызов этого метода (при этом можно и attrs.xml удалить) и в xml разметке, где обьявлен ваш view, добавить android:scrollbars="horizontal|vertical", то скролбары будут тоже видны (проверено на Android 4.4.3). Пример
Но при таком подходе скроллинг будет работать только в одном направлении, в зависимости от направления жеста (вверх-вниз или вправо-влево).
В данной статье приведен пример готового виджета-заготовки, который скролится нормально в любом направлении, содержит скроллбары и поддерживает жест "бросок".
Для поддержки скроллбаров в папке проекта /res/values/ создайте файл attrs.xml со следующим содержанием:
Открыть спойлер
Ниже привожу полный код класса-шаблона от которого потом следует наследовать свои View.- package ua.naiksoftware.widget;
- import android.content.Context;
- import android.content.res.TypedArray;
- import android.util.AttributeSet;
- import android.view.GestureDetector;
- import android.view.MotionEvent;
- import android.view.View;
- import android.widget.Scroller;
- /**
- * @author Naik
- */
- public abstract class AnyScroll extends View {
- private Scroller scroller;// считает скроллинг
- private GestureDetector gestureDetector; // определяет жесты
- private int w, h;// размер поля, которое требуется скролить
- private int scrW, scrH;//видимый размер view'а
- private int scrollX, scrollY;// координаты скроллинга
- public AnyScroll(Context context) {
- super(context);
- init(context);
- }
- public AnyScroll(Context context, AttributeSet attr) {
- super(context, attr);
- init(context);
- }
- public AnyScroll(Context context, AttributeSet attr, int style) {
- super(context, attr, style);
- init(context);
- }
- // Начальная инициализация.
- private void init(Context context) {
- scroller = new Scroller(context);
- gestureDetector = new GestureDetector(context, new MyGestureListener());
- /* следующий код можно удалить, если не нужны скроллбары */
- setVerticalScrollBarEnabled(true);
- setHorizontalScrollBarEnabled(true);
- TypedArray a = context.obtainStyledAttributes(R.styleable.View);
- initializeScrollbars(a);
- }
- /**
- * Устанавливаем размер поля, которое будет скролится
- *
- * @param width ширина ви пикселях
- * @param height высота в пикселях
- */
- public void setSize(int width, int height) {
- w = width;
- h = height;
- }
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- int action = event.getAction();
- if (action == MotionEvent.ACTION_DOWN) {
- if (!scroller.isFinished()) {
- // если во время "броска" нажали на экран, то останавливаем скроллинг
- scroller.abortAnimation();
- }
- }
- gestureDetector.onTouchEvent(event);
- return true;
- }
- // Вызывается системой для пересчета скроллинга.
- @Override
- public void computeScroll() {
- if (scroller.computeScrollOffset()) {
- int x = scroller.getCurrX();
- int y = scroller.getCurrY();
- scrollTo(x, y);
- if (scrollX != getScrollX() || scrollY != getScrollY()) {
- // Если изменились координаты, то обновляем координаты.
- onScrollChanged(getScrollX(), getScrollY(), scrollX, scrollY);
- }
- invalidate();
- }
- }
- @Override
- protected void onScrollChanged(int l, int t, int oldl, int oldt) {
- scrollX = l;
- scrollY = t;
- }
- /* Наш детектор нажатий */
- private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
- @Override
- public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanseX, float distanseY) {
- if (scrollX + distanseX > 0 && scrollX + distanseX < w - getWidth()) {
- // если не заскролили за наше поле, то двигаем по абсциссе
- scrollBy((int) distanseX, 0);
- }
- if (scrollY + distanseY > 0 && scrollY + distanseY < h - getHeight()) {
- // то же, но по ординате
- scrollBy(0, (int) distanseY);
- }
- return true;
- }
- @Override
- public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
- scroller.fling(scrollX, // начало броска по х
- scrollY, // начало броска по у
- -(int) velocityX, // скорость по x
- -(int) velocityY, // скорость по у
- 0, // минимальная возможная координата для скроллинга по ширина
- w - scrW, // максимальная
- 0, // то же, но по высоте
- h - scrH);
- invalidate(); // обновляем экран
- return true;
- }
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- int rawx = (int) e.getX() + scrollX;
- int rawy = (int) e.getY() + scrollY;
- onTapUp(rawx, rawy);// уведомляем наследника о одиночном тапе, по аналогии можно и
- postInvalidate(); // для других жестов так сделать
- return true;
- }
- };
- /**
- * Переопределите для получения координат одиночного нажатия с учетом
- * скроллинга
- *
- * @param x расстояние от 0 до места нажатия с учетом скроллинга
- * @param y расстояние от 0 до места нажатия с учетом скроллинга
- */
- protected void onTapUp(int x, int y) {
- }
- // Следующие 6 методов нужны только для скроллбаров, если они не нужны -- можете удалять.
- @Override
- protected int computeHorizontalScrollExtent() {
- return scrW;
- }
- @Override
- protected int computeHorizontalScrollOffset() {
- return scrollX;
- }
- @Override
- protected int computeHorizontalScrollRange() {
- return w;
- }
- @Override
- protected int computeVerticalScrollExtent() {
- return scrH;
- }
- @Override
- protected int computeVerticalScrollOffset() {
- return scrollY;
- }
- @Override
- protected int computeVerticalScrollRange() {
- return h;
- }
- // получаем новый размер view, например при повороте устройства.
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- scrW = getWidth();
- scrH = getHeight();
- }
- }
Открыть спойлер
Обновление
В Android Lolipop метод initializeScrollbars удален. Если удалить вызов этого метода (при этом можно и attrs.xml удалить) и в xml разметке, где обьявлен ваш view, добавить android:scrollbars="horizontal|vertical", то скролбары будут тоже видны (проверено на Android 4.4.3). Пример
- <com.example.MyScrollCustomView
- android:id="@+id/my_view"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:scrollbars="horizontal|vertical"/>