Статьи

Android SDK: введение в жесты

Одним из наиболее распространенных изменений в использовании устройств с сенсорным экраном за последние пару лет стало использование жестов, таких как пролистывание и взмах. Они позволяют взаимодействию между пользователем и устройством стать очень интуитивным и естественным ощущением. Из этого руководства вы узнаете, как начать использовать жесты в своих собственных приложениях для Android.

В этом руководстве будет использоваться код, предоставленный в проекте с открытым исходным кодом . Авторы предполагают, что читатель имеет некоторый опыт работы с Android и Java. Однако, если у вас есть вопросы о том, что мы сделали, не стесняйтесь спрашивать.

Из этого туториала вы узнаете, как начать обрабатывать жесты пальцами в своих приложениях. Мы будем делать это с помощью простого рисования объекта Canvas на пользовательском объекте View. Этот метод может быть применен к любой графической среде, которую вы используете, будь то 2D-поверхность или даже рендеринг OpenGL ES. Если вы заинтересованы в мультитач-жестах (более продвинутая тема обработки жестов), мы расскажем об этом в следующем уроке.

Давайте начнем с простого. Создайте новый проект Android. Мы назвали наш проект Gesture Fun и настроили его одно действие, которое называется GestureFunActivity. Измените файл макета по умолчанию, main.xml, на следующий, очень простой макет:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<?xml version=»1.0″ encoding=»utf-8″?>
<LinearLayout
    xmlns:android=»http://schemas.android.com/apk/res/android»
    android:orientation=»vertical»
    android:layout_width=»fill_parent»
    android:layout_height=»fill_parent»>
    <TextView
        android:layout_width=»fill_parent»
        android:layout_height=»wrap_content»
        android:text=»Drag the droid around» />
    <FrameLayout
        android:id=»@+id/graphics_holder»
        android:layout_height=»match_parent»
        android:layout_width=»match_parent»></FrameLayout>
</LinearLayout>

Важным элементом в этом макете является определение FrameLayout. Элемент управления FrameLayout используется для хранения пользовательского представления, которое будет рисовать изображение.

Наконец, давайте обновим метод onCreate () класса Activity, чтобы инициализировать элемент управления FrameLayout и предоставить ему некоторое содержимое:

1
2
3
4
5
6
7
8
9
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    FrameLayout frame = (FrameLayout) findViewById(R.id.graphics_holder);
    PlayAreaView image = new PlayAreaView(this);
    frame.addView(image);
}

На данный момент мы еще не определили класс PlayAreaView, поэтому проект не будет компилироваться. Имейте терпение, мы вернемся к этому на следующем шаге.

Со всеми настройками нашего проекта теперь мы можем сосредоточиться на интересной части: рисование на объекте Canvas. Простой способ получить объект Canvas для рисования — переопределить метод onDraw () объекта View. Удобно, что этот метод имеет единственный параметр: объект Canvas. Рисование растрового изображения на объекте Canvas так же просто, как вызов метода drawBitmap () объекта Canvas. Вот простой пример реализации метода onDraw (), как определено в нашем новом классе PlayAreaView:

01
02
03
04
05
06
07
08
09
10
private class PlayAreaView extends View {
    private Matrix translate;
    private Bitmap droid;
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(droid, translate, null);
        Matrix m = canvas.getMatrix();
        Log.d(DEBUG_TAG, «Matrix: «+translate.toShortString());
        Log.d(DEBUG_TAG, «Canvas: «+m.toShortString());
    }
}

Наша реализация метода onDraw () довольно проста. Как обычно, вам нужно определить переменную тега ведения журнала DEBUG_TAG где-нибудь в вашей активности. Большая часть метода onDraw () — это просто информационный вывод. Единственная реальная работа, выполняемая в этом методе, происходит в вызове drawBitmap (), где первым параметром является изображение для рисования. Второй параметр — это объект Matrix с именем translate, который, как следует из названия, указывает, где будет отображаться растровое изображение относительно представления, в котором находится объект Canvas. Весь остальной код в этом руководстве будет включать манипулирование матрицей перевода на основе определенных событий касания пользователя. Это, в свою очередь, изменит место рисования объекта Bitmap в Canvas и, следовательно, на экране.

Класс PlayAreaView нуждается в конструкторе для выполнения начальной настройки. Так как наш пользовательский вид должен реагировать на жесты, нам нужен GestureDetector. GestureDetector — это класс Android, который может принимать события движения, выполнять некоторую математическую магию, чтобы определить, что они есть, а затем делегировать вызовы объекта GestureListener в качестве определенного жеста или других обратных вызовов движения. Объект GestureListener, реализуемый нами класс, получает эти вызовы для определенных жестов, которые распознает GestureDetector, и позволяет нам реагировать на них так, как мы считаем нужным (в данном случае, для перемещения графики внутри нашего PlayAreaView). Хотя GestureDetector обрабатывает определенные движения, он не выполняет с ними ничего конкретного и не обрабатывает все типы жестов. Тем не менее, для целей данного руководства, он предоставляет достаточно информации. Итак, давайте подключим это:

1
2
3
4
5
6
7
8
public PlayAreaView(Context context) {
    super(context);
    translate = new Matrix();
    gestures = new GestureDetector(GestureFunActivity.this,
            new GestureListener(this));
    droid = BitmapFactory.decodeResource(getResources(),
            R.drawable.droid_g);
}

Давайте посмотрим на конструктор PlayAreaView немного подробнее. Сначала мы инициализируем матрицу перевода в единичную матрицу (по умолчанию). Напомним, что единичная матрица не будет вносить изменений в растровое изображение: оно будет отображаться в своем первоначальном местоположении
Затем мы создаем и инициализируем GestureDetector — объект по умолчанию — и назначаем ему действительный объект GestureListener (мы поговорим об этом чуть позже). Наконец, растровое изображение, называемое droid, загружается непосредственно из ресурсов проекта. Вы можете использовать любое изображение — бейсбол, яблоко, печенье с предсказанием и т. Д. Это рисунок, который вы будете использовать для объекта Canvas.

Далее мы перейдем к GestureListener, так как это пользовательский объект. А пока давайте подключим объект GestureDetector, чтобы он получал данные движения, необходимые для распознавания магии его жеста.

Теперь давайте подключим объект GestureDector, называемый жестами, для получения событий. Для этого переопределите метод onTouchEvent () элемента управления View в классе PlayAreaView следующим образом:

1
2
3
4
@Override
public boolean onTouchEvent(MotionEvent event) {
    return gestures.onTouchEvent(event);
}

Здесь мы сделали GestureDetector последним словом во всех сенсорных событиях для этого пользовательского представления. Однако GestureDetector на самом деле ничего не делает с событиями движения, он просто распознает их и делает вызов зарегистрированному классу GestureListener.

Чтобы реагировать на события, распознаваемые классом GestureDetector, нам необходимо реализовать класс GestureListener. События движения, которые нас больше всего интересуют, представляют собой двойные касания и жесты любого рода. Чтобы прослушивать эти типы событий движения, наш класс GestureListener должен реализовывать интерфейсы OnGestureListener и OnDoubleTapListener.

1
2
3
4
5
6
7
private class GestureListener implements GestureDetector.OnGestureListener,
        GestureDetector.OnDoubleTapListener {
    PlayAreaView view;
    public GestureListener(PlayAreaView view) {
        this.view = view;
    }
}

После добавления этого класса в качестве подкласса Activity добавьте реализации по умолчанию для каждого из необходимых методов. Например, вот реализация для метода onDown ():

1
2
3
4
5
@Override
public boolean onDown(MotionEvent e) {
    Log.v(DEBUG_TAG, «onDown»);
    return true;
}

Реализация этих методов позволяет изучать различные события по мере их распознавания объектом GestureDetector. Интересно, что если метод onDown () не возвращает true, основной интересующий нас жест — прокрутка (или перетаскивание) — не будет обнаружена. Однако вы можете вернуть false для других признанных событий, которые вас не интересуют.

Объект MotionEvent, который передается в качестве параметра каждому методу обратного вызова, иногда представляет событие касания, которое запустило распознавание жеста, а иногда — последнее событие, которое завершило распознавание жеста. Для наших целей мы позволяем классу GestureDetector обрабатывать все детали расшифровки, какой тип движения представляет MotionEvent.

Примечание. Платформа Android также предоставляет вспомогательный класс SimpleOnGestureListener, который объединяет два интерфейса (OnGestureListener & OnDoubleTapListener) в один класс с реализациями по умолчанию для всех методов. Реализации по умолчанию возвращают false.

Первое событие, которое мы хотели бы обработать, это событие прокрутки. Событие прокрутки происходит, когда пользователь касается экрана, а затем перемещает палец по нему. Этот жест также известен как событие перетаскивания. Это событие приходит через метод onScroll () интерфейса OnGestureListener.

Вот реализация метода onScroll ():

1
2
3
4
5
6
7
8
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
        float distanceX, float distanceY) {
    Log.v(DEBUG_TAG, «onScroll»);
 
    view.onMove(-distanceX, -distanceY);
    return true;
}

Используйте событие прокрутки для передачи запроса на перемещение в объект PlayAreaView. Реализация этого метода является важным первым шагом в отображении того, как событие движения пальца вызывает движение графики. Мы вернемся к этому в ближайшее время. А пока ты справился со своим первым жестом!

Графика будет перемещаться по всему экрану — а иногда даже за его пределами. По определению, изображение видно только в том случае, если оно нарисовано в пределах объекта View. Если координаты графики попадают за границы объекта View, рисунок обрезается (не отображается). Вы можете добавить обнаружение контуров и другую логику (мы боимся, что это выходит за рамки этого урока), или просто добавить обнаружение для двойного касания и сбросить местоположение графики. Вот пример реализации метода onDoubleTap () (из интерфейса OnDoubleTapListener):

1
2
3
4
5
6
@Override
public boolean onDoubleTap(MotionEvent e) {
    Log.v(DEBUG_TAG, «onDoubleTap»);
    view.onResetLocation();
    return true;
}

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

Жест броска, по сути, оставляет скорость на предмете, который перетаскивается по экрану. Движущийся элемент обычно постепенно замедляется, но такое поведение диктуется реализацией разработчика. Например, в игре скорость может зависеть от физики игрового мира. В других приложениях скорость может быть основана на любой формуле, подходящей для действия, которое она представляет. Тестирование — лучший способ понять, каково это. По нашему опыту, некоторые проб и ошибок необходимы для того, чтобы остановиться на чем-то, что чувствует — и выглядит — как раз правильно.

В нашей реализации мы собираемся изменить отрезок времени до того, как движение, вызванное броском, прекратится, а затем просто запустим анимацию изображения до конечного пункта назначения на основе скорости, переданной нам методом onFling (), и количество времени, которое мы установили. Помните, что жест броска не обнаруживается до тех пор, пока палец пользователя больше не будет касаться дисплея. Думайте об этом как о броске камня — камень продолжает двигаться, когда вы его отпускаете, — это та часть, которую мы хотим оживить, когда пользователь «отпускает».

Звучит сложно? Вот код:

01
02
03
04
05
06
07
08
09
10
11
12
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
        final float velocityX, final float velocityY) {
    Log.v(DEBUG_TAG, «onFling»);
    final float distanceTimeFactor = 0.4f;
    final float totalDx = (distanceTimeFactor * velocityX/2);
    final float totalDy = (distanceTimeFactor * velocityY/2);
 
    view.onAnimateMove(totalDx, totalDy,
            (long) (1000 * distanceTimeFactor));
    return true;
}

Нам даже не нужно проверять два параметра MotionEvent, данных о скорости достаточно для наших целей. Единицы скорости в пикселях в секунду. Таким образом, мы можем использовать данные о скорости, чтобы выбрать коэффициент масштабирования, который будет использоваться для определения конечной продолжительности времени, прежде чем изображение полностью остановится. В нашем случае мы используем 40% секунды (400 мс). Таким образом, умножив половину двух значений скорости на 40% (или переменную distanceTimeFactor), мы получим общее движение, достигнутое за эту долю секунды. Наконец, мы передаем эту информацию нашему пользовательскому методу onAnimateMove () объекта View, который фактически заставляет нашу графику перемещаться по экрану, используя информацию, предоставленную событием движения движения.

Почему половина начальной скорости? Если мы начинаем, скажем, со скорости A и заканчиваем со скоростью B в течение любого промежутка времени, средняя скорость равна (A + B) / 2. В этом случае конечная скорость равна 0. Таким образом, мы сокращаем скорость пополам, чтобы изображение не случайно выглядело так, будто оно отскакивает от нашего пальца быстрее, чем оно было до того, как мы его выпустили.

Почему 400 мс? Нет причин вообще, но на большинстве устройств это выглядит довольно неплохо. Это не то же самое, что калибровка движений мыши — слишком быстрая, и она кажется взволнованной и плохо различимой, слишком медленной, и вы ждете, пока ваш медленный указатель мыши не догонит ваш мозг. Это значение является основной переменной, чтобы настроить «чувство» вашего броска. Чем выше значение, тем меньше будет «трение», которое будет иметь изображение при скольжении на экране. Если у вас есть реальные вариации поверхности, вам нужно применять регулярные физические расчеты. Здесь мы просто делаем фиксированную функцию замедления без реальной физики.

Теперь, когда все жесты, которые нас интересуют в области обработки, пришло время реализовать фактическое движение базовой графики. Вернувшись в класс PlayAreaView, добавьте метод onMove ():

1
2
3
4
public void onMove(float dx, float dy) {
    translate.postTranslate(dx, dy);
    invalidate();
}

Этот метод делает две вещи. Во-первых, он переводит (translate = графический термин для перемещения из точки A в точку B) нашу собственную матрицу на расстояние, на которое переместился палец. Затем он делает недействительным представление, так что оно будет перерисовано. При рисовании изображение будет отображаться в другом месте в представлении. Если бы мы хотели панорамировать весь объект View, мы могли бы использовать метод translate () объекта View, чтобы обновить его внутреннюю матрицу, используемую его Canvas, для выполнения всего рисования. Это могло бы хорошо работать для некоторых вещей, но если бы у нас были некоторые статические (под которыми мы имеем в виду неподвижные, неподвижные, неподвижные, как горы) вещи внутри вида, это было бы не так. Вместо этого для этого случая мы просто обновляем нашу собственную Матрицу, переводим, которую мы используем каждый раз, когда рисуем графику.

Теперь мы добавим также метод onResetLocation() :

1
2
3
4
public void onResetLocation() {
    translate.reset();
    invalidate();
}

Этот метод просто сбрасывает матрицу в единичную матрицу и заставляет представление снова перерисовываться с помощью метода invalidate (). Когда представление перерисовывается, изображение возвращается в исходное положение.

Для движения броска у нас есть немного больше, чем просто нарисовать его в новом месте. Мы хотим, чтобы оно оживило эту позицию. Плавное движение может быть достигнуто с помощью анимации, то есть очень быстро рисует изображение в другом месте. В Android есть встроенные классы анимации, но они применяются ко всем представлениям. Мы не анимируем объект View. Вместо этого мы перемещаем изображение на холсте, контролируемом видом. Итак, мы должны реализовать нашу собственную анимацию. Штопать.

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

Давайте начнем с onAnimateMove() :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
private Matrix animateStart;
private Interpolator animateInterpolator;
private long startTime;
private long endTime;
private float totalAnimDx;
private float totalAnimDy;
 
public void onAnimateMove(float dx, float dy, long duration) {
    animateStart = new Matrix(translate);
    animateInterpolator = new OvershootInterpolator();
    startTime = System.currentTimeMillis();
    endTime = startTime + duration;
    totalAnimDx = dx;
    totalAnimDy = dy;
    post(new Runnable() {
        @Override
        public void run() {
            onAnimateStep();
        }
    });
}

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

Здесь мы инициализируем нашу анимацию, используя класс OvershootInterpolator. Так как этот интерполятор заставляет изображение перемещаться немного дальше, в целом, чем мы рассчитывали, технически изображение начнется немного быстрее. Это небольшая разница, которую не следует замечать, но если вы стремитесь к точности точности, вам нужно подстроиться под нее (что будет означать написание собственного интерполятора —
выходит за рамки этого урока — и имеет метод для расчета общего пройденного расстояния).

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

Все эти вычисления выполняются с помощью метода onAnimateStep (), показанного ниже. Мы вызываем метод onAnimationStep () через сообщение в очереди сообщений. Мы не хотим зацикливаться — мы бы заставили систему перестать отвечать на запросы. Итак, простой способ — просто опубликовать сообщения. Это позволяет системе оставаться отзывчивой, обеспечивая асинхронное поведение без необходимости работать с потоками. Поскольку в любом случае мы должны рисовать в потоке пользовательского интерфейса, в этом простом примере нет никакого смысла для потока.

Теперь давайте реализуем метод onAnimateStep() :

01
02
03
04
05
06
07
08
09
10
11
12
13
private void onAnimateStep() {
   long curTime = System.currentTimeMillis();
   float percentTime = (float) (curTime — startTime)
           / (float) (endTime — startTime);
   float percentDistance = animateInterpolator
           .getInterpolation(percentTime);
   float curDx = percentDistance * totalAnimDx;
   float curDy = percentDistance * totalAnimDy;
   translate.set(animateStart);
   onMove(curDx, curDy);
 
   Log.v(DEBUG_TAG, «We’re » + percentDistance + » of the way there!»);
   if (percentTime

Сначала мы определяем процент времени прохождения анимации, сохраняемый в переменной с плавающей точкой процентаTime. Затем мы используем эти данные, чтобы интерполятор мог сказать нам, где мы находимся в процентах от начала и до конца, сохраняемые как переменная с плавающей точкой, называемая процентным сопротивлением. Затем мы используем эти данные, чтобы определить, где мы находимся в пикселях вдоль оси x и y от начальной позиции, сохраняемые как curDx и curDy (обозначающие текущую дельту x и текущую дельту y). Матрица перевода затем сбрасывается до исходного значения, которое мы сохранили (animateStart). Наконец, метод onMove () используется с вычисленными curDx и curDy, чтобы фактически переместить графику в ее следующую позицию вдоль анимации. Уф!

Интерполятор не дает расстояние, измененное от предыдущего местоположения. Вместо этого он дает только текущий процент завершения всего пройденного расстояния. Вот почему мы используем наше известное начальное местоположение. Наконец, если это еще не сделано, отправьте вызов onAnimateStep ().

Примечание . Рассмотрите возможность изучения других интерполяторов, предоставляемых платформой Android. Линейный интерполятор просто замедляет движение равномерно на расстоянии. Это даст точное общее пройденное расстояние, которое мы рассчитали. Попробуйте и посмотрите, чем он отличается от Overshoot Interpolator.

Это все, что нужно сделать. Если вы следовали своему собственному коду, вы можете убедиться, что ввели все, ознакомившись с нашим открытым исходным кодом. В противном случае, когда вы запустите его, ваше изображение будет анимировать что-то похожее на то, что вы видите в этом видео:

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

Мы с нетерпением ждем ваших отзывов.