среда, 19 декабря 2012 г.

Анаграммы на Android

Как-то застал жену за придумыванием всех слов , которые можно составить из букв слова «лекарство» ( flash-игра такая), немного помог ей в этом.  Мысль об автоматизации возникла сразу, смысл игры при этом, конечно, теряется, но стало интересно  - насколько много слов можно составить и насколько быстро такой поиск будет выполняться на смартфоне.
Для начала нашел словарик (в текстовом формате) из  47612 существительных в единственном числе, именительном падеже . Словарь конечно  попался кривоватый – многих слов нет, зато есть, например, слово «та» (то ли русское местоимение, то ли буква арабского алфавита, то ли знак каны). Этот словарик будем загружать в массив строк при старте программы.
Для введенного слова посчитаем количество для каждой из букв. Для каждой буквы словарного слова тоже будем считать количество и, если для всех букв словарного слова это количество будет меньше или равно аналогичному  значению для введенного слова, то словарное слово нам подходит.  Частным случаем тут будет поиск анаграмм – когда найденное словарное слово имеет ту же длину,  что и исходное. Возможно, существует и более производительный алгоритм, но реализовал то, что сразу пришло в голову.
В итоге для слова «лекарство» за 3 секунды на HTC Desire было найдено 175 вариантов.
Ниже исходник получившейся программы.

/**
 * Anagram Builder
 * Copyright (C) 2012 Anmyst
 * @author Anmyst
 * @version 1.0
*/

package com.anmyst.anagramru;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;

/**
 * Anagram Builder activity
 */
public class AnagramruActivity extends Activity {

 private ArrayList <String> arr; //словарь
 private ArrayAdapter <String> adapt; //список результатов
 private HashMap map; // хранит количество букв для каждой буквы в слове

 /**
  * Запуск поиска анаграмм
  */
 private OnClickListener ButtonStart_click = new View.OnClickListener(){
 
  @Override
  public void onClick(View v) {
   final EditText eWord = (EditText)findViewById(R.id.editTextWord);
   String sWord = eWord.getText().toString();
   if (sWord.equalsIgnoreCase(""))
    return;
  
   String sDict;
   adapt.clear();
   map.clear();
  
   for(int i = 0; i < sWord.length(); i++){
    map.put(sWord.charAt(i), CharCount(sWord,sWord.charAt(i)));
   }
  
   boolean anagram = false;
   final CheckBox chkAnagram = (CheckBox)findViewById(R.id.checkBoxAnagram);
   if (chkAnagram.isChecked())
    anagram = true;
  
   int key;
   int ok;
   int counter = 0;
   for(int j = 0; j < arr.size(); j++){
    sDict = arr.get(j);
    ok = 1;
    for (int k = 0; k < sDict.length(); k++){
    
     if(map.containsKey(sDict.charAt(k)))
      key = (Integer)map.get(sDict.charAt(k));
     else
      key = 0;
     if (CharCount(sDict, sDict.charAt(k)) > key){
      ok = 0;
      continue;
     }
    }
    if (ok == 1){
     if ((!anagram) || (sDict.length() == sWord.length())){     
       adapt.add(sDict);
       counter++;     
     }     
    }
    
   }
   showToast("Found:" + counter);
  
  }
 };

 /**
  * Вывод короткого сообщения
  * @param s Строка сообщения
  */
 private void showToast(String s){ 
  Toast mess = Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT);
  mess.show(); 
 }

 /**
  * Считает количество заданных символов в строке
  * @param s исходное слово
  * @param c символ для которого расчитывается количество
  * @return количество заданных символов в слове
  */
 private int CharCount(String s, char c){
  int cnt = 0;
  for (int i = 0; i < s.length(); i++){
   if (s.charAt(i) == c)
    cnt++;
  }
  return cnt;
 }

 /**
  * Чтение строк из файла словаря
  * в массив arr
  * @param fname имя файла
  */
 private int ReadFile(String fname)
 {
  String s1;
  try{
   File f = new File(Environment.getExternalStorageDirectory() + "/DictRu/" + fname);
   FileInputStream fileIS = new FileInputStream(f);
   BufferedReader buf = new BufferedReader(new InputStreamReader(fileIS, "windows-1251"));
   String readString = new String();
   while((readString = buf.readLine()) != null){
    s1 = readString.toLowerCase();
    arr.add(s1);
   }
  } catch (FileNotFoundException e) {
      e.printStackTrace();
      return 1;
   } catch (IOException e){
    e.printStackTrace();
      return 2;
   }
  return 0;
 }

 /**
  * Загрузить словарь из файла
  */
 private void LoadDictionary(){
  arr.clear();
  if (ReadFile("dictru.txt") != 0){
   showToast("Load Dictionary Error!");
  }
  else{
   showToast("Dictionary loaded!");
  }
  return;
 }

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
   
    final Button bStart = (Button)findViewById(R.id.buttonStart);
    bStart.setOnClickListener(ButtonStart_click);
   
    map = new HashMap();
   
    arr = new ArrayList <String>();
    adapt = new ArrayAdapter<String> (this,android.R.layout.simple_list_item_1,android.R.id.text1);
    adapt.setNotifyOnChange(true);
   
    ListView list = (ListView)this.findViewById(R.id.listViewResults);
    list.setAdapter(adapt);
   
    LoadDictionary();
}

}

суббота, 11 февраля 2012 г.

Приглядываемся к программированию под Android.


Приглядываемся к программированию под 
Android.

Так случилось, что попался мне в лапы HTC Desire, и захотелось мне разобраться с программированием под этого зверя. Не берусь учить кого-то программировать под Android,  так что прошу рассматривать все ниженаписанное как хронику попыток освоения Android в промежутках между работой, семьей и прочими радостями жизни.  Рекомендуется к прочтению начинающим (возможно, это сэкономит некоторое время и позволит обойти пару-тройку граблей) и профессиональным программистам (может кто-то ткнет меня в явные косяки и сэкономит мне некоторое количество времени и нервов).

Стартуем.

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

Книги:

Быстрый старт http://itblog.name/archives/678
Сразу упомяну весьма полезный блог http://megadarja.blogspot.com/
Ну и конечно http://developer.android.com

На выходе имеем настроенную среду Eclipse с Android SDK и маленькое, но гордое приложение “Hello Android!”. Ради всего святого, не надо публиковать его на Google Market, там такого добра хватает.

Особых проблем пока не возникло, правда, чтобы избавиться от постоянной ошибки DDMS при соединения с устройством (похоже на упорные попытки использования ipv6)  пришлось дописать в eclipse.ini:

-Djava.net.preferIPv4Stack=true 

На следующем этапе почитаем какую-нибудь книжку, параллельно пробуем примеры из книжки. Хорошо бы конечно прочитать книжку целиком, но времени мало поэтому для начала ограничимся главами по использованию базовых элементов управления.

Проба пера.

А теперь попробуем сделать что-нибудь разумное, доброе, но простое. Для начала решил сделать нечто несложное, но нужное для себя.  Итак, задача №1 – делаю форму для поиска по справочнику на несколько тысяч контактов. Справочник выгрузил в csv-файлы. И хранить эти файлы будем в выделенной директории на SD-карте, таким образом, мы всегда сможем добавить дополнительные файлы контактов. Считаем, что все файлы с расширением txt в этой директории – это файлы контактов. Поиск будем делать не по каждому полю (ФИО, должность, телефон, e-mail),  а по всей строке. Результаты выведем в табличном виде. Если результатов больше 20, то смысла в этом немного, поэтому и выведем только первые 20 и сообщение.

Сейчас нам понадобится не только учебник, но и справочник. В качестве справочника использовал http://developer.android.com и начал понимать откуда берут(мягко говоря) многие примеры авторы книг. А еще мне пригодился учебник по java. Ну и конечно активно используем поиск.

Я не буду описывать размещение элементов управления, обработку нажатия на кнопку «Поиск» и т.п., расскажу только о возникших проблемах.

1.  Сходу не удалось ничего записать на SD-карту. При запуске эмулятора обнаружил, что никаких прав на подключенную SD-карту нет.

После выполнения remount sd карты из командной строки все нормализовалось.

adb shell
mount -o remount,rw /sdcard 

2. Работа с GridView

Для начала надо разобраться с написанием адаптера для GridView. Довольно содержательная информация по этому вопросу есть на http://megadarja.blogspot.com/ . 
Нам нужен адаптер, который будет наполняться данными из файлов и очищаться при каждом следующем поиске.

В итоге получился вот такой кусок кода:

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
   
    final Button buttonFind = (Button)findViewById(R.id.ButtonFind);
    buttonFind.setOnClickListener(buttonFind_click);
                 
    GridView grid = (GridView)this.findViewById(R.id.GridView01);
    Icounter = 0;
   
    arr = new ArrayList <String>();
    adapt = new ArrayAdapter<String> (this, R.layout.grid_item, arr);
    adapt.setNotifyOnChange(true);
    grid.setAdapter(adapt);
}

При нажатии на кнопку поиска очищаем адаптер adapt.clear().
После получения из файла строк контактов, добавляем строки:

adapt.add("Файл");
adapt.add(s1);
adapt.add("ФИО");
adapt.add(s2);
adapt.add("должность");
adapt.add(s3);
adapt.add("моб.тлф.");
adapt.add(s4);
adapt.add("стац.тлф");
adapt.add(s5);
adapt.add("e-mail");
adapt.add(s6);


Вызвало некоторое недоумение отсутствие рамки в таблице. Без рамки таблица смотрится как-то некрасиво. Для того чтобы сделать рамку пришлось создать разметку с указанием фона для  ячейки таблицы и отступов, а также задать фон для всей таблицы.

Разметка для ячейки таблицы  (grid_item.xml):

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
       android:id="@+id/GridText01"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="10dp"
    android:background="#000050"
    android:textSize="10sp" >
</TextView>

И все равно получилось кривовато если в одной строке в столбцах ячейки с разным количеством строк.

Займемся 2D графикой.

Ну что же попробуем нарисовать что-нибудь не слишком уродливое. Заодно подумаем, как можно от простого рисования перейти к чему-то игроподобному.  Самое время начать строительство скелета, на базе которого будем потом строить любое приложение с 2D графикой. В сторону готовых Framework-ов пока не смотрим, сейчас надо разобраться, как тут все устроено, чтобы потом не страдать хронической зависимостью. В плане скелетов опять очень помог блог http://megadarja.blogspot.com/ . По нему  строил модель приложения  на основе класса, унаследованного от SurfaceView, и потока для отрисовки содержимого.  Тут наткнулся на проблему некорректного завершения приложения при повторном запуске. Вылечил вызовом finish() в обработчике OnStop для activity.

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

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

Задача №2 – делаем фотопаззл. Берем несколько фотографий (для начала взял свое семейство – мама, папа, дочка, сын) и разрезаем их на прямоугольники. При старте приложения формируем массив наших прямоугольных областей и для каждой области задаем по кусочку от каждой фотографии. При прорисовке экрана выводим изображения для всех областей. При нажатии на экран меняем состояние для соответствующего участка изображения и, соответственно, отображается кусочек от следующей фотографии.
Ну что же, дочка с удовольствием поигралась с этой штукой.


Выходим на рынок.


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

Все в этом приложении будет состоять из прямоугольников – фон стартовой страницы, кнопки для выбора паззлов, сами паззлы. Таким образом, для инициализации каждого окна просто формируем нужный массив прямоугольников и устанавливаем дополнительные флаги. После выбора паззла на стартовой странице перемешиваем кусочки изображения в случайном порядке.  А отрисовываться все это будет одной функцией.

Инициализация окна паззла выглядит таким образом:

public void initWin2(){
       
        GObject o;
        int id1, id2, id3, id4, id5;
        int i, j;
       
        win2flag = 1;
        doneflag = 0;
        obs.clear();
       
        Resources res = mContext.getResources();
       
        id1 = R.drawable.city01_01;
        id2 = R.drawable.city02_01;
        id3 = R.drawable.city03_01;
        id4 = R.drawable.city04_01;
        id5 = R.drawable.city05_01;
       
        messbuf[0].delete(0, messbuf[0].length());
        messbuf[0].append("Dome of the Rock, Jerusalem, Israel");
        messbuf[1].delete(0, messbuf[1].length());
        messbuf[1].append("Notre Dame de Paris, France");
        messbuf[2].delete(0, messbuf[2].length());
        messbuf[2].append("Karlstejn Castle, Czech Republic");
        messbuf[3].delete(0, messbuf[3].length());
        messbuf[3].append("Peterhof, St.Petersburg, Russia");
        messbuf[4].delete(0, messbuf[4].length());
        messbuf[4].append("Charles Bridge, Prague, Czech Republic");

       
        for (i = 0; i<4; i++)
        {
            for (j=0; j<6; j++)
            {
                o = new GObject();
                o.x1 = 40 + j * 120;
                o.y1 = i * 120;
                o.x2 = o.x1 + 120;
                o.y2 = o.y1 + 120;
                o.SetDrawable(1, res.getDrawable(id1));
                o.SetDrawable(2, res.getDrawable(id2));
                o.SetDrawable(3, res.getDrawable(id3));
                o.SetDrawable(4, res.getDrawable(id4));
                o.SetDrawable(5, res.getDrawable(id5));
                id1++;
                id2++;
                id3++;
                id4++;
                id5++;
                o.clickable = 1;
                o.n_images = 5;
                o.state = rnd.nextInt(o.n_images) + 1;
                o.id = 100;
                obs.add(o);
            }
           
        }
    }

После сборки паззла выдаем сообщение с описанием фотографии и при следующем клике опять все перемешиваем.

Для коротких сообщений очень удобно пользовать toast-ами.

Toast mess = Toast.makeText(mContext, messbuf[r1-1], Toast.LENGTH_SHORT);
mess.show();

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

Вспоминаем про неприятный факт наличия груды разнообразных Android-устройств с различными разрешениями экрана, соотношениями сторон и начинаем с ним бороться.  В качестве базового разрешения я взял родное разрешение «своего» устройства – в горизонтальной ориентации 800х480.  Теперь надо рассчитывать координаты наших прямоугольников с учетом реального разрешения экрана.

float ax=(canvas.getWidth()/(float)800.0);
float ay=(canvas.getHeight()/(float)480.0);
            
float fx1 = x1 * ax;
float fx2 = x2 * ax;
float fy1 = y1 * ay;
float fy2 = y2 * ay;

img1.setBounds((int)(fx1), (int)(fy1), (int)(fx2), (int)(fy2));
img1.draw(canvas);

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


Ну что же, пора выложить то, что получилось на Google Market. На текущий момент на Украине недоступно создание Google Merchant аккаунта для продажи приложений. Поэтому регистрируем обычный, да и требовать деньги за получившийся фотопаззл бессмысленно  и неприлично.

Для регистрации нужно:
1.      Аккаунт gmail.
2.       Веб-сайт (страничка на blogspot отлично подходит).
3.      Заплатить $25 (использовал карточку Visa).

Процесс регистрации, прошел очень быстро и беспроблемно, пошаговых материалов по нему немеряно.

Теперь загружаем приложение, и при выборе apk-файла выясняется, что его нужно еще и подписать цифровой подписью. Отладочный ключ для этого не годится. Создание хранилища ключей, собственно ключа и подписание apk-файла можно выполнить прямо в Eclipse.

А еще нужно подготовить минимум 2 скриншота (проще всего использовать форму DDMS в Eclipse) и большую иконку приложения 512х512.