View со свободным скроллингом

от
Android   view, scrollview

Часто при создании игр требуется игровое поле, карта, или что-нибудь другое, что будет скролиться во всех направлениях. Для этого в разметке нужный View можно обернуть в контейнер ScrollView и одновременно HorizontalScrollView.
Но при таком подходе скроллинг будет работать только в одном направлении, в зависимости от направления жеста (вверх-вниз или вправо-влево).
В данной статье приведен пример готового виджета-заготовки, который скролится нормально в любом направлении, содержит скроллбары и поддерживает жест "бросок".

Для поддержки скроллбаров в папке проекта /res/values/ создайте файл attrs.xml со следующим содержанием:
Открыть спойлер
Ниже привожу полный код класса-шаблона от которого потом следует наследовать свои View.
  1. package ua.naiksoftware.widget;
  2.  
  3. import android.content.Context;
  4. import android.content.res.TypedArray;
  5. import android.util.AttributeSet;
  6. import android.view.GestureDetector;
  7. import android.view.MotionEvent;
  8. import android.view.View;
  9. import android.widget.Scroller;
  10.  
  11. /**
  12.  * @author Naik
  13.  */
  14. public abstract class AnyScroll extends View {
  15.  
  16.     private Scroller scroller;// считает скроллинг
  17.     private GestureDetector gestureDetector; // определяет жесты
  18.  
  19.     private int w, h;// размер поля, которое требуется скролить
  20.     private int scrW, scrH;//видимый размер view'а
  21.     private int scrollX, scrollY;// координаты скроллинга
  22.  
  23.     public AnyScroll(Context context) {
  24.         super(context);
  25.         init(context);
  26.     }
  27.  
  28.     public AnyScroll(Context context, AttributeSet attr) {
  29.         super(context, attr);
  30.         init(context);
  31.     }
  32.  
  33.     public AnyScroll(Context context, AttributeSet attr, int style) {
  34.         super(context, attr, style);
  35.         init(context);
  36.     }
  37.  
  38.     // Начальная инициализация.
  39.     private void init(Context context) {
  40.         scroller = new Scroller(context);
  41.         gestureDetector = new GestureDetector(context, new MyGestureListener());
  42.         /* следующий код можно удалить, если не нужны скроллбары */
  43.         setVerticalScrollBarEnabled(true);
  44.         setHorizontalScrollBarEnabled(true);
  45.         TypedArray a = context.obtainStyledAttributes(R.styleable.View);
  46.         initializeScrollbars(a);
  47.     }
  48.  
  49.     /**
  50.      * Устанавливаем размер поля, которое будет скролится
  51.      *
  52.      * @param width ширина ви пикселях
  53.      * @param height высота в пикселях
  54.      */
  55.     public void setSize(int width, int height) {
  56.         w = width;
  57.         h = height;
  58.     }
  59.  
  60.     @Override
  61.     public boolean onTouchEvent(MotionEvent event) {
  62.         int action = event.getAction();
  63.         if (action == MotionEvent.ACTION_DOWN) {
  64.             if (!scroller.isFinished()) {
  65.                 // если во время "броска" нажали на экран, то останавливаем скроллинг
  66.                 scroller.abortAnimation();
  67.             }
  68.         }
  69.         gestureDetector.onTouchEvent(event);
  70.         return true;
  71.     }
  72.  
  73.     // Вызывается системой для пересчета скроллинга.
  74.     @Override
  75.     public void computeScroll() {
  76.         if (scroller.computeScrollOffset()) {
  77.             int x = scroller.getCurrX();
  78.             int y = scroller.getCurrY();
  79.             scrollTo(x, y);
  80.             if (scrollX != getScrollX() || scrollY != getScrollY()) {
  81.                 // Если изменились координаты, то обновляем координаты.
  82.                 onScrollChanged(getScrollX(), getScrollY(), scrollX, scrollY);
  83.             }
  84.             invalidate();
  85.         }
  86.     }
  87.  
  88.     @Override
  89.     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
  90.         scrollX = l;
  91.         scrollY = t;
  92.     }
  93.  
  94.    /* Наш детектор нажатий */
  95.     private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
  96.  
  97.         @Override
  98.         public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanseX, float distanseY) {
  99.             if (scrollX + distanseX > 0 && scrollX + distanseX < w - getWidth()) {
  100.                 // если не заскролили за наше поле, то двигаем по абсциссе
  101.                 scrollBy((int) distanseX, 0);
  102.             }
  103.             if (scrollY + distanseY > 0 && scrollY + distanseY < h - getHeight()) {
  104.                 // то же, но по ординате
  105.                 scrollBy(0, (int) distanseY);
  106.             }
  107.             return true;
  108.         }
  109.  
  110.         @Override
  111.         public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
  112.             scroller.fling(scrollX, // начало броска по х
  113.                                           scrollY, // начало броска по у
  114.                                           -(int) velocityX, // скорость по x
  115.                                           -(int) velocityY, // скорость по у
  116.                                           0, // минимальная возможная координата для скроллинга по ширина
  117.                                           w - scrW, // максимальная
  118.                                           0, // то же, но по высоте
  119.                                           h - scrH);
  120.             invalidate(); // обновляем экран
  121.             return true;
  122.         }
  123.  
  124.         @Override
  125.         public boolean onSingleTapUp(MotionEvent e) {
  126.             int rawx = (int) e.getX() + scrollX;
  127.             int rawy = (int) e.getY() + scrollY;
  128.             onTapUp(rawx, rawy);// уведомляем наследника о одиночном тапе, по аналогии можно и
  129.             postInvalidate();           // для других жестов так сделать
  130.             return true;
  131.         }
  132.     };
  133.  
  134.     /**
  135.      * Переопределите для получения координат одиночного нажатия с учетом
  136.      * скроллинга
  137.      *
  138.      * @param x расстояние от 0 до места нажатия с учетом скроллинга
  139.      * @param y расстояние от 0 до места нажатия с учетом скроллинга
  140.      */
  141.     protected void onTapUp(int x, int y) {
  142.     }
  143.  
  144.     // Следующие 6 методов нужны только для скроллбаров, если они не нужны -- можете удалять.
  145.     @Override
  146.     protected int computeHorizontalScrollExtent() {
  147.         return scrW;
  148.     }
  149.  
  150.     @Override
  151.     protected int computeHorizontalScrollOffset() {
  152.         return scrollX;
  153.     }
  154.  
  155.     @Override
  156.     protected int computeHorizontalScrollRange() {
  157.         return w;
  158.     }
  159.  
  160.     @Override
  161.     protected int computeVerticalScrollExtent() {
  162.         return scrH;
  163.     }
  164.  
  165.     @Override
  166.     protected int computeVerticalScrollOffset() {
  167.         return scrollY;
  168.     }
  169.  
  170.     @Override
  171.     protected int computeVerticalScrollRange() {
  172.         return h;
  173.     }
  174.  
  175.    // получаем новый размер view, например при повороте устройства.
  176.     @Override
  177.     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  178.         super.onSizeChanged(w, h, oldw, oldh);
  179.         scrW = getWidth();
  180.         scrH = getHeight();
  181.     }
  182. }
И напоследок привожу пример использования класса:
Открыть спойлер

Обновление
     В Android Lolipop метод initializeScrollbars удален. Если удалить вызов этого метода (при этом можно и attrs.xml удалить) и в xml разметке, где обьявлен ваш view, добавить android:scrollbars="horizontal|vertical", то скролбары будут тоже видны (проверено на Android 4.4.3). Пример
  1. <com.example.MyScrollCustomView
  2.                android:id="@+id/my_view"
  3.                android:layout_width="match_parent"
  4.                android:layout_height="wrap_content"
  5.                android:scrollbars="horizontal|vertical"/>
+7   7   0
2572