Для одного из текущих проектов понадобилось сделать контрол для множественного выбора элементов из списка.
Делать это отдельной Activity как-то не хотелось, но в Android нет готового компонента для этого. Максимум, что можно сделать, это использовать AlertDialog с множественным выбором. Обычный же Spinner позволяет выбрать только один элемент.
И вот, после гугления было найдено неплохое решение на StackOverflow, которое позволяет совместить внешний вид Spinner’а и функционал AlertDialog’а. Решение для Java можно посмотреть здесь. Это решение мне не очень понравилось из-за того, что работает только со строками + необходимо использовать дополнительный метод для установки элементов списка для выбора, вместо того, чтобы нормально испольщовать Adapter для этого.

После небольших допиливаний получился контрол, который, все-таки, можно использовать с адаптером. Правда, при этом теряется возможность хранить ссылку на объект адаптера где-то снаружи контрола и делать ему NotifyDataSetChanged() и NotifyDataSetInvalidated(), но для спиннера это очень редко используемый функционал, так что можно назвать решение приемлемым.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.Util;

namespace SampleWidgets.Android.Views
{
    public class MultiSpinner : Spinner, 
		IDialogInterfaceOnMultiChoiceClickListener, 
		IDialogInterfaceOnCancelListener
    {
        public class MultiSpinnerSelectionEventArgs : EventArgs
        {
            public MultiSpinnerSelectionEventArgs(bool[] selected)
            {
                Selected = selected;
            }

            public bool[] Selected { get; private set; }
        }



        public delegate void ItemsSelectedHandler(object sender, 
			MultiSpinnerSelectionEventArgs args);
        public event ItemsSelectedHandler ItemsSelected;
        private ISpinnerAdapter RealAdapter;
        private bool[] selected;

        public MultiSpinner(IntPtr a, JniHandleOwnership b) : base(a, b) { }

        public MultiSpinner(Context context) : base(context)
        {
        }

        public MultiSpinner(Context context, IAttributeSet attrs) 
			: base(context, attrs)
        {
        }

        public MultiSpinner(Context context, IAttributeSet attrs, int defStyle) 
			: base(context, attrs, defStyle)
        {
        }

        public void OnClick(IDialogInterface dialog, int which, bool isChecked)
        {
            selected[which] = isChecked;
        }

        private ISpinnerAdapter CreateLabelAdapter()
        {
            List<string> names = new List<string>();
            int count = RealAdapter != null ? RealAdapter.Count : 0; 
            for (int i = 0; i < count; i++)
            {
                if (selected[i]) names.Add(RealAdapter.GetItem(i).ToString());
            }
            string label = string.Join(", ", names);
            if (label.Length == 0) 
				label = Context.GetString(Resource.String.LabelNone);
            return new ArrayAdapter<string>(Context,
                Android.Resource.Layout.sherlock_spinner_item,
                new string[] { label });
        }

        public void OnCancel(IDialogInterface dialog)
        {
            base.Adapter = CreateLabelAdapter();
            if (ItemsSelected != null) 
				ItemsSelected(this, new MultiSpinnerSelectionEventArgs(selected));
        }

        public override bool PerformClick()
        {
            AlertDialog.Builder builder = new AlertDialog.Builder(Context);
            List<string> names = new List<string>();
            int count = RealAdapter != null ? RealAdapter.Count : 0;
            if (count > 0)
            {
                for (int i = 0; i < count; i++)
                {
                    names.Add(RealAdapter.GetItem(i).ToString());
                }
                builder.SetMultiChoiceItems(names.ToArray(), selected, this);
                builder.SetPositiveButton(global::Android.Resource.String.Ok,
                    delegate(object o, DialogClickEventArgs e) 
					{
						(o as AlertDialog).Cancel(); 
					});
                builder.SetOnCancelListener(this);
                builder.Show();
            }
            return true;
        }

        public override ISpinnerAdapter Adapter 
        { 
            get { return RealAdapter; } 
            set 
            {
                selected = new bool[value.Count]; 
                RealAdapter = value;  
                base.Adapter = CreateLabelAdapter(); 
            } 
        }
    }
}

В коде можно заметить странного вида конструктор public MultiSpinner(IntPtr a, JniHandleOwnership b). Без него Mono for Android не хочет компилировать этот компонент т.к. в нем переопределены виртуальные методы и свойства. Решение проблемы с компиляцией было найдено на том же StackOverflow в этом топике. Ответ, который помечен как решение проблемы, хоть и описывает суть проблемы, но не предлагает реального решения. Зато добавление конструктора реально помогает.

Выглядит все это как-то так:
multi-spinner-screenshot

Previous Article
Технический директор IT-Dimension, компании-разработчика кросс-платформенного программного обеспечения

Leave a Reply

Your email address will not be published. Required fields are marked *

О.

О сверхдешевых ноутбуках и о том как не надо делать бизнес – Часть II

Ага, а вот и продолжение истории о ноутбуках “для всех”. Первую часть можно почитать здесь.

Итак, покупка. Как это было? Было это мм.. довольно странно. Договаривался я с братьсями-китайцами две недели, письма писать они умеют, пишут вежливо: “Dear Sir, …” и все такое. В общем-то две недели общения посредством e-mail даром не прошли, информации я узнал об устройствах достаточно и это только укрепило мое желание сделать покупку. Сказали стоимость доставки: $224 если отправлять с помощью EMS (Express Mail Service). Почему именно EMS? Потому что они доставляют посылки каким-то чудесным образом и за них не надо платить таможенный сбор, который у нас может составоять до 30% от стоимости посылки. Именно из-за таможенных сборов мне пришлось отказаться от отправки с помощью DHL, хотя, конечно, я им как-то больше верю, сколько раз с ними работал, всегда все получалось, а FedEx не рассматривался по той причине что качество предоставляемых услуг у них ээ.. как бы это сказать, ниже плинтуса.

Да, в общем сошлись на EMS. Посчитаю-ка я общую стоимость, что у нас получается $162 * 5 + 224 итого $1034. Это получается почти по $207 за устройство. Фигасе!, – подумал я, а как же ж “для всех”? А как же ж “сверхдешевый да еще и на Linux’е”? Это ж 2/3 стоимости EEE PC! Но делать нечего, “Двадцать тыщ уже уплочено” (с) Масяня.

Прислали инвойс. Перевод сделал ПриватБанком. На удивление довольно шустро все произошло, за 40 минут мне открыли лицевой счет (по-моему 30 гривен это стоило, около $6) и $12 взяли за перевод международный. Я даже удивился, в ПриватБанке обычно все ооочень медленно, а тут вот так получилось шустро. Хоть что-то приятное. Итого $1034 + 6 + 12 получается $1052. Это по $210 за штучку. М-да.. почти в два раза стоимость увеличилась от заявленной.

Итак, денежка ушла в Китай. В тот же день отправил братьям-китайцам скан квитанции об оплате и получил от них “большое спасибо, технологические образцы будут готовы в течении 2х недель”.

Ну OK, две так две, можно и подождать, до нового года еще далеко 🙂

Прошло две недели, отписываю им, мол “как там дела с железками”, на что получаю, что-то типа “мы тут как бы пытаемся выпустить новую версию устройства со встроенным Wi-FI модулем, давайте мы вам новые пришлем образцы, но чуть позже”. Слово “Фигасе!” почему-то оять посетило мою голову, при чем тут встроенный вайфай? Я ж проплатил внешний. Ну, говорю, смотрите, я вам за внешний заплатил, дайте мне чего я хочу, просто вышлите и все. В ответ тишина…

Прошла неделя, опять им пишу, мол чего там с железом-то? Я тут заждался уже! На что получаю ответ “ну вот мы тут еще и экранчик улучшили, стало больше цветов и разрешение улучшилось и там еще помните, встроенный вайфай… но этот время, мы это все готовим… но если чего, можем вернуть вам деньги”. Ооо, отлично, экранчик.. но как же $60 за внешние вайфай модули? В общем, решил подождать, обещали разрешение 800х480 (оказывается раньше было меньше!).

Подождал до начал аоктября, опять тишина. Отписываю, мол, ну как там? Уже сделали? Опять тишина. Но я настойчивый, каждые три дня писал им письма 🙂 Наконец-то вот три дня назад получил ответ что мы отправили DHL’ом. Трекинг намбер бла-бла-бла.

Оооо! Отправили! Но DHL! Это получается что за все это добро мне прийдется платить таможенный сбор! Ну ёкарныбабай, мы же договаривались! С таким раскладом мне эти устройства выйдут по стоимости как EEE PC! Решил было успокоиться, но не ту-то было. На сайте DHL указанный tracking number оказался невалидным. Для пущей уверенности пошел проверить на сайте EMS, может в письме ошиблись с названием компании-курьера… Попробовал, то же самое, невалидный номер.

Сегодня отписал им по этому поводу, жду ответа, нервничаю…

Продолжение этой истории напишу как появятся новые сведения.

ЗЫ: У кого-нибудь еще есть опыт покупки оборудования в Китае? Как это было?

Р.

Работа с акселерометром в Android

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

Для работы с различными датчиками в Android используется класс Sensor. Список датчиков можно получить через SensorManager. Например таким вот образом при создании Activity можно получить объект Sensor, связанный с акселеромтером:

public class AccelerometerTest extends Activity {

	SensorManager mSensorManager;
	Sensor mAccelerometerSensor;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mSensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
        List<Sensor> sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);
        if(sensors.size() > 0)
        {
        	for (Sensor sensor : sensors) {
        		switch(sensor.getType())
        		{
        		case Sensor.TYPE_ACCELEROMETER:
        			if(mAccelerometerSensor == null) mAccelerometerSensor = sensor;
        			break;
        		default:
        			break;
        		}
		}
        }
    }


Для того, чтобы получать данные с акселерометра нам необходимо проделать еще несколько несложных операций:

  • Реализовать интерфейс SensorEventListener
  • , в частности нас интересует метод onSensorChanged()

  • Реализовать метод onResume() где подписать Activity на сообщения от акселеромтера
  • Реализовать метод onPause() где отписать Activity от сообщений акселерометра
public class AccelerometerTest extends Activity implements SensorEventListener {
    @Override
    protected void onPause() {
    	mSensorManager.unregisterListener(this);
    	super.onPause();

    }

    @Override
    protected void onResume() {
    	super.onResume();
    	mSensorManager.registerListener(this, mAccelerometerSensor, SensorManager.SENSOR_DELAY_GAME);
    	mSensorManager.registerListener(this, mMagneticFieldSensor, SensorManager.SENSOR_DELAY_GAME);
    }

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {
	}

	@Override
	public void onSensorChanged(SensorEvent event) {
		float [] values = event.values;
		switch(event.sensor.getType())
		{
		case Sensor.TYPE_ACCELEROMETER:
			{
				// Здесь можно обрабатывать данные от сенсора
			}
			break;
		}
	}
}

Простейший пример отображения данных – отображать их в TextView

public class AccelerometerTest extends Activity implements SensorEventListener {
	SensorManager mSensorManager;
	Sensor mAccelerometerSensor;

	TextView mForceValueText;
	TextView mXValueText;
	TextView mYValueText;
	TextView mZValueText;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		...
		mForceValueText = (TextView)findViewById(R.id.value_force);
		mXValueText = (TextView)findViewById(R.id.value_x);
		mYValueText = (TextView)findViewById(R.id.value_y);
		mZValueText = (TextView)findViewById(R.id.value_z);
	}
	...
	@Override
	public void onSensorChanged(SensorEvent event) {
		float [] values = event.values;
		switch(event.sensor.getType())
		{
		case Sensor.TYPE_ACCELEROMETER:
			{
				mXValueText.setText(String.format("%1.3f", 
					event.values[SensorManager.DATA_X]));
				mYValueText.setText(String.format("%1.3f", 
					event.values[SensorManager.DATA_Y]));
				mZValueText.setText(String.format("%1.3f", 
					event.values[SensorManager.DATA_Z]));

				double totalForce = 0.0f;
				totalForce += Math.pow(
					values[SensorManager.DATA_X]/SensorManager.GRAVITY_EARTH, 2.0);
				totalForce += Math.pow(
					values[SensorManager.DATA_Y]/SensorManager.GRAVITY_EARTH, 2.0);
				totalForce += Math.pow(
					values[SensorManager.DATA_Z]/SensorManager.GRAVITY_EARTH, 2.0);
				totalForce = Math.sqrt(totalForce);
				mForceValueText.setText(String.format("%1.3f", totalForce));
			}
			break;
		}
	}
}

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<TableLayout 	android:id="@+id/TableLayout01"
				android:layout_width="fill_parent"
				android:layout_height="fill_parent"
				xmlns:android="http://schemas.android.com/apk/res/android">
	<TableRow 	android:layout_width="wrap_content"
				android:layout_height="wrap_content"
				android:id="@+id/row_force"
				android:layout_margin="5dip">
		<TextView 	android:layout_width="wrap_content"
					android:layout_height="wrap_content"
					android:id="@+id/label_force"
					android:text="Force:"
					android:gravity="right"></TextView>
		<TextView 	android:layout_height="wrap_content"
					android:layout_width="fill_parent"
					android:layout_weight="1"
					android:id="@+id/value_force"
					android:text="-"
					android:layout_marginLeft="5dip"></TextView>
	</TableRow>
	<TableRow 	android:layout_height="wrap_content"
				android:layout_width="fill_parent"
				android:id="@+id/row_x"
				android:layout_margin="5dip">
		<TextView 	android:layout_width="wrap_content"
					android:layout_height="wrap_content"
					android:text="X:"
					android:id="@+id/label_x"
					android:gravity="right"></TextView>
		<TextView	android:layout_height="wrap_content"
					android:text="-"
					android:layout_width="fill_parent"
					android:layout_weight="1"
					android:id="@+id/value_x"
					android:layout_marginLeft="5dip"></TextView>
	</TableRow>
	<TableRow 	android:layout_height="wrap_content"
				android:layout_width="fill_parent"
				android:id="@+id/row_y"
				android:layout_margin="5dip">
		<TextView 	android:layout_width="wrap_content"
					android:layout_height="wrap_content"
					android:text="Y:"
					android:id="@+id/label_y"
					android:gravity="right"></TextView>
		<TextView 	android:layout_height="wrap_content"
					android:text="-"
					android:layout_width="fill_parent"
					android:layout_weight="1"
					android:id="@+id/value_y"
					android:layout_marginLeft="5dip"></TextView>
	</TableRow>
	<TableRow 	android:layout_height="wrap_content"
				android:layout_width="fill_parent"
				android:id="@+id/row_z"
				android:layout_margin="5dip">
		<TextView	android:layout_width="wrap_content"
					android:layout_height="wrap_content"
					android:text="Z:"
					android:id="@+id/label_z"
					android:gravity="right"></TextView>
		<TextView 	android:layout_height="wrap_content"
					android:text="-"
					android:layout_width="fill_parent"
					android:layout_weight="1"
					android:id="@+id/value_z"
					android:layout_marginLeft="5dip"></TextView>
	</TableRow>
</TableLayout>

В результате получим что-то подобное:

Из примера можно увидеть что в классе SensorManager есть константы DATA_X, DATA_Y, DATA_Z, которые используются в качетсве индексов в массиве значений, возвращаемых акселерометром.
Отображение данных в TextView – это, конечно, неплохо, но не дает общей картины изменений показаний акселерометра при изменении положения телефона. Для того, чтобы увидеть изменение показаний во времени, решил добавить отображение в виде графика.
Для создания графиков набрел на чудесную библиотеку AChartEngine. Библиотека бесплатная, доступна на Google Code.
Добавляем в layout пару кнопок – для начала/останова записи показаний акселерометра и для открытия окна с графиком.

<?xml version="1.0" encoding="utf-8"?>
<TableLayout 	android:id="@+id/TableLayout01"
				android:layout_width="fill_parent"
				android:layout_height="fill_parent"
				xmlns:android="http://schemas.android.com/apk/res/android">
...
<TableRow 	android:id="@+id/TableRow01"
				android:layout_height="wrap_content"
				android:layout_width="fill_parent">
		<ViewStub	android:id="@+id/ViewStub01"
					android:layout_width="wrap_content"
					android:layout_height="wrap_content"></ViewStub>
		<LinearLayout 	android:id="@+id/LinearLayout01"
						android:layout_height="wrap_content"
						android:layout_width="fill_parent"
						android:layout_weight="1">
			<Button 	android:layout_height="wrap_content"
						android:layout_weight="1"
						android:text="Start recording"
						android:layout_width="fill_parent"
						android:id="@+id/button_start"></Button>
			<Button 	android:layout_height="wrap_content"
						android:layout_weight="1"
						android:text="Show"
						android:layout_width="fill_parent"
						android:id="@+id/button_show"></Button>
		</LinearLayout>
	</TableRow>
</TableLayout>

В результате этих изменений получаем такой layout:

Теперь научим Activity реагировать на нажания кнопок:

package com.itdimension.accelerometertest;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

import org.achartengine.*;
import org.achartengine.chart.PointStyle;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.model.XYSeries;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;

public class AccelerometerTest extends Activity implements SensorEventListener {
	...
	double margins[] = {0, 0};

	Button mStartButton;
	Button mShowButton;

	List<List<Double>> mValues;
	boolean mIsRecording = false;

	OnClickListener mStartButtonListener = new OnClickListener() {

		@Override
		public void onClick(View v) {
			mIsRecording = !mIsRecording;
			if(mIsRecording) {
				mValues.get(SensorManager.DATA_X).clear();
				mValues.get(SensorManager.DATA_Y).clear();
				mValues.get(SensorManager.DATA_Z).clear();
				margins[0] = 0;
				margins[1] = 0;
			}
		}
	};

	OnClickListener mShowButtonListener = new OnClickListener() {

		@Override
		public void onClick(View v) {
			try
			{
				Intent intent = getChartIntent();
				startActivity(intent);
			}
			catch (Exception e) {
				new AlertDialog.Builder(AccelerometerTest.this)
					.setTitle("Error")
					.setMessage(e.getMessage())
					.create()
					.show();
			}

		}
	};

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        ...
        mValues = new ArrayList<List<Double>>();
        mValues.add(new ArrayList<Double>());
        mValues.add(new ArrayList<Double>());
        mValues.add(new ArrayList<Double>());
	...
        mStartButton = (Button)findViewById(R.id.button_start);
        mShowButton = (Button)findViewById(R.id.button_show);

        mStartButton.setOnClickListener(mStartButtonListener);
        mShowButton.setOnClickListener(mShowButtonListener);
    }
    ...

	@Override
	public void onSensorChanged(SensorEvent event) {
		float [] values = event.values;
		switch(event.sensor.getType())
		{
		case Sensor.TYPE_ACCELEROMETER:
			{
				if(mIsRecording)
				{
					recordSensorValue(event);
				}
				...
			}
			break;
		}
	}

	private void recordSensorValue(SensorEvent event) {
		double value;
		for(int i = SensorManager.DATA_X; i <= SensorManager.DATA_Z; i++)
		{
			value = (double)event.values[i];
			margins[0] = Math.min(margins[0], value);
			margins[1] = Math.max(margins[1], value);
			mValues.get(i).add(value);
		}
	}

	Intent getChartIntent() {
		int [] colors = new int[] { 
		      Color.RED, Color.GREEN, Color.BLUE };
		PointStyle[] styles = new PointStyle[] { 
		      PointStyle.POINT, PointStyle.POINT, PointStyle.POINT };
		XYMultipleSeriesRenderer renderer = buildRenderer(colors, styles);
		setChartSettings(renderer, "Sensor Values", "Index", "Value",
	    		0,
	    		mValues.get(SensorManager.DATA_X).size(),
	    		margins[0] * 1.5,
	    		margins[1] * 1.5,
	        	Color.GRAY, Color.LTGRAY);
		return ChartFactory.getLineChartIntent(this, buildDataset(), renderer);
	}

	protected void setChartSettings(XYMultipleSeriesRenderer renderer, 
		      String title, String xTitle,
		      String yTitle, double xMin, 
		      double xMax, double yMin, double yMax, 
		      int axesColor, int labelsColor) {
		    renderer.setChartTitle(title);
		    renderer.setXTitle(xTitle);
		    renderer.setYTitle(yTitle);
		    renderer.setXAxisMin(xMin);
		    renderer.setXAxisMax(xMax);
		    renderer.setYAxisMin(yMin);
		    renderer.setYAxisMax(yMax);
		    renderer.setAxesColor(axesColor);
		    renderer.setLabelsColor(labelsColor);
		  }

	protected XYMultipleSeriesRenderer buildRenderer(int[] colors, PointStyle[] styles) {
	    XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
	    int length = colors.length;
	    for (int i = 0; i < length; i++) {
	      XYSeriesRenderer r = new XYSeriesRenderer();
	      r.setColor(colors[i]);
	      r.setPointStyle(styles[i]);
	      renderer.addSeriesRenderer(r);
	    }
	    return renderer;
	  }

	XYMultipleSeriesDataset buildDataset() {
		XYMultipleSeriesDataset result = new XYMultipleSeriesDataset();
		XYSeries xSeries = new XYSeries("X");
		XYSeries ySeries = new XYSeries("Y");
		XYSeries zSeries = new XYSeries("Z");

		int count = mValues.get(SensorManager.DATA_X).size();
		for(int i = 0; i < count; i++)
		{
			xSeries.add(i, mValues.get(SensorManager.DATA_X).get(i));
			ySeries.add(i, mValues.get(SensorManager.DATA_Y).get(i));
			zSeries.add(i, mValues.get(SensorManager.DATA_Z).get(i));
		}

		result.addSeries(xSeries);
		result.addSeries(ySeries);
		result.addSeries(zSeries);

		return result;
	}
}

После всех этих манипуляций, при нажатии на кнопку “Show” получим приблизительно такой график:


Ну вот, на этом пока все.
Скачать исходный код примера можно здесь.