пятница, 14 января 2011 г.

Даблклик по виджету на Android

Как известно, даблклика (даблтапа - double tap) на андроиде нет. Но порой требуется, чтобы одинарный и двойной щелчок по виджету приводили к разным действиям. Приходится реализовывать даблклик обходным путем. Например, так, как описано ниже.

Идея обходного пути проста. Заводим статический синглетон. В синглетоне регистрируем клики по виджиту. При первом клике запускается таймер. Если таймер срабатывает до того, как произойдет второй клик, мы запускаем функцию обработки одинарного клика. Если же раньше произойдет второй клик, запускаем функцию обработки double click, а последующее событие таймера игнорируем.

Layout для виджета выглядит так:
<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:gravity="center" 
 android:orientation="vertical" 
 android:id="@+id/widget_layout"
 android:layout_width="72dip"
   android:layout_height="72dip"
 android:clickable="true">
 <ImageView android:id="@+id/widget_image_view"
     android:scaleType="fitXY"
     android:layout_width="fill_parent" 
     android:layout_height="fill_parent" 
     android:layout_marginLeft="0.0dip"
     android:src="@drawable/link_pressed"
  />
</RelativeLayout>
Т.е. виджет содержит компонент ImageView, который умеет обрабатывать клики. Вот как выглядит класс виджета:
public class MyProvider extends AppWidgetProvider {

private static final String ACTION_WIDGET_CONTROL = "my.domain.WIDGET_CONTROL";
public static final String ACTION_UPDATE_WIDGET_CONTEXT = "my.domain.UPDATE_WIDGET_CONTEXT";
public static final String URI_SCHEME = "my.domain";      

public static final String PARAM_WIDGET_ID = "my.domain.appWidgetId";
public static final String PARAM_WIDGET_POSITION = "my.domain.widgetPos";

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    for (int widget_id : appWidgetIds) {
      update_widget_view(context, widget_id, appWidgetManager);                  
    }                  
    super.onUpdate(context, appWidgetManager, appWidgetIds);        
}

private void set_click_listener_to_widget(Context context, int widgetId, RemoteViews remoteViews) {
//обработчик одинарного клика по виджету
 Intent active = new Intent(context, AWidgetProvider.class);
 //при нажатии на ImageButton на виджите
 //будет посылаться ACTION_WIDGET_CONTROL
 //мы перехватим его в OnReceive
 active.setAction(ACTION_WIDGET_CONTROL); 
 Uri data = Uri.parse(URI_SCHEME + "://widget#");
 data = data.buildUpon()
  .appendQueryParameter("widget_id", String.valueOf(widgetId))
  .build();
 active.setData(data);
 PendingIntent pi = PendingIntent.getBroadcast(context, 0, active, 0);
 remoteViews.setOnClickPendingIntent(R.id.widget_image_view, pi); 
}                 
    
@Override
public void onReceive(Context context, Intent intent) {
 final String action = intent.getAction();
 ....
 if (action.equals(ACTION_WIDGET_CONTROL)) {
//обрабатываем клик по виджету
    Uri uri = intent.getData();
    //определяем позицию виджета на экране
    Rect widget_pos = intent.getSourceBounds(); 
    int widget_id = Integer.parseInt(uri.getQueryParameter("widget_id"));
    if (widget_id != AppWidgetManager.INVALID_APPWIDGET_ID) {
      on_widget_click(context, widget_id, widget_pos);
    }
 } 
 super.onReceive(context, intent);
}       
   
private boolean on_widget_click(Context context, int widgetId, Rect widgetPos) {
//Регистрируем все клики по виджету
  return ((Kernel)context.getApplicationContext())
     .RegisterClick(widgetId, widgetPos);
}

private void update_widget_view(Context context, int widgetId, AppWidgetManager appWidgetManager) {
 RemoteViews remote_views = new RemoteViews(context.getPackageName()
     , R.layout.widget);
 set_click_listener_to_widget(context, widgetId, remote_views); 
 appWidgetManager.updateAppWidget(widgetId, remote_views);
}

public static void makeDoubleClickAction(Context context
  , int appWidgetId, Rect widgetPos) {
//!TODO: обработка одинарного клика
}

public static void makeSingleClickAction(Context context
  , int appWidgetId, Rect widgetPos) {
//!TODO: Обработка двойного клика
}
}
Переходим к синглетону, в котором мы будет регистрировать клики. Синглетон создаем на базе класса Application - во-первых, чтобы избежать проблем с использованием синглетона в разных Activity, во-вторых, чтобы получить доступ к контексту, который нам нужен (скорее всего) в функциях makeDoubleClickAction и makeSingleClickAction. Код синглетона:
public final class Kernel extends Application { 
//http://stackoverflow.com/questions/708012/android-how-to-declare-global-variables
public static final String TAG_LOG = "my.domain";

public static int DOUBLE_CLICK_DELAY_MS = 200; 

//номер сессии - порядковый номер кликов
//номер сессии нужен, чтобы избежать ложного срабатывания функции "один клик" по таймеру уже
//после того, как сработала функция "дабл клик".
private static int m_DoubleClickSession = 1;
private static int m_DoubleClickCountClicks = 0;
private static DoubleClickHelper m_DoubleClickHelper = null;

/* Регистрирует клик. Возвращает True, если это был первый клик в последовательности double-click.
 * */
public boolean RegisterClick(int appWidgetId, Rect widgetRect) {
 synchronized (this) {
  if (m_DoubleClickCountClicks == 1) {
   m_DoubleClickSession++;
   m_DoubleClickCountClicks = 0;
   m_DoubleClickHelper = null; 
   MyProvider.makeDoubleClickAction(this.getApplicationContext()
      , appWidgetId, widgetRect);
   return false;
  } else {
   m_DoubleClickHelper = new DoubleClickHelper(m_DoubleClickSession
     , appWidgetId, widgetRect);
   Timer timer = new Timer();
   timer.schedule(m_DoubleClickHelper, Kernel.DOUBLE_CLICK_DELAY_MS);
   m_DoubleClickCountClicks = 1;
   return true;
  }
 }
}

private void on_first_click_timer(int sessionId, int appWidgetId, Rect widgetRect) {
 synchronized (this) {
  //если номер сессии изменился, значит пока мы ждали таймер, был второй клик
  //и была вызвана функция дабл-клика. В этом случае, ничего не вызываем.
  if (Kernel.m_DoubleClickSession == sessionId) { 
   m_DoubleClickSession++;
   m_DoubleClickCountClicks = 0;
   m_DoubleClickHelper = null;
   MyProvider.makeSingleClickAction(this.getApplicationContext(), appWidgetId, widgetRect);
  }
 }  
}

public class DoubleClickHelper  extends TimerTask {
    final int m_AppWidgetId;
    final Rect m_WidgetRect;
    final int m_DoubleClickSession;
    
 public DoubleClickHelper(int doubleClickSession, int appWidgetId, Rect widgetRect) {
  m_AppWidgetId = appWidgetId;
  m_WidgetRect = widgetRect;
  m_DoubleClickSession = doubleClickSession;
 }
 
 @Override 
 public void run() {
  on_first_click_timer(m_DoubleClickSession, m_AppWidgetId, m_WidgetRect);
 }  
}
}
Значение задержки таймера DOUBLE_CLICK_DELAY_MS лучше вынести в настройки, ведь скорее всего, на разных устройствах потребуется задавать разные значения задержки.

Функции makeSingleClickAction и makeDoubleClickAction могут быть объявлены как угодно. В моем проекте требовалось передать в них в качестве параметров контекст и позицию виджета на экране. Отсюда многочисленные Rect в коде - если вам позиция виджета не важна, можете смело выкидывать все Rect-ы.

Приведенный выше код вполне успешно работает в приложении Animated Widget Contact Launch. Тем не менее, ошибки не исключены. Вполне возможно, что со временем они выплывут - тогда я о них обязательно сообщу. Если же вы найдете ошибку, буду признателен за сообщение о ней в комментариях.

Update: как оказалось, приведенный выше код работает вовсе не так хорошо, как хотелось бы. Дело оказалось в 200 миллисекундной задержке, которая возникает при клике по виджету (пока ждем второй клик). Она слишком заметна и субъективно кажется что виджет тормозит. Пользователи стали жаловаться. Пришлось выкручиваться: по первому клику сразу же запускать однокликовое действие (запуск активити с анимацией) и, одновременно, продолжать отслеживать второй клик. Особенность: отслеживать второй клик приходится как на стороне виджета, так и на стороне запущенной активити, т.к. на практике второй клик может прийти и туда, и туда.

2 комментария:

  1. Лучше и проще сделать тап зоны для нескольких действий.

    ОтветитьУдалить
  2. Согласен. Под Android лучше стараться обходиться без даблклика.

    ОтветитьУдалить