четверг, 2 декабря 2010 г.

Android widget, part 0.4

Может перескакиваю немного, но по ощущениям где-то так :) – 0.4

Мне понадобилось добавить в свой виджет диалог выбора папки с файлами; стандартного контрола/класса не оказалось, так что пришлось 2 вечера гуглить, вот что нагуглил; но сначала ТЗ уточненное: я каким-то образом говорю “хочу выбрать папку”, должен появится список доступных в телефоне папок с файлами, причем я хочу а) ограничится только картой памяти и б) обрабатывать только тапы по папкам, но в) хочу показывать в папке файлы, если они – файлы картинок (для меня картинки == jpg и png

Сделал тестовый проект, начинаю с окошка с кнопкой и текстовым полем, куда выведу результат выбора – разметка:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_height="fill_parent"
        android:layout_width="fill_parent">
    <TextView
        android:id="@+id/resultText"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="..."
        />
    <Button
        android:id="@+id/button"
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:text="Select directory"
        />
</LinearLayout>

и класс:

package info.hamster.filelist;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class TestActivity extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test);

        Button btnSelect = (Button)findViewById(R.id.button);
        btnSelect.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View view) {
                Intent intent = new Intent(TestActivity.this, FileListActivity.class);
                startActivityForResult(intent, 7);
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

         if (requestCode == 7) {
             TextView text = (TextView)findViewById(R.id.resultText);
             if (resultCode == RESULT_OK) {
                 Bundle res = data.getExtras();
                 text.setText(res.getString("dir"));
             } else {
                 text.setText("Canceled");
             }
         }

    }
}

По тапе на кнопку я вызываю активити выбора папки и жду от него результат работы; число “7” выбрано “от фонаря” и должно различать ответы от разных активити (если их много) в одном обработчике

Диалог выбора папки представляет собой список и кнопки Up, Ok и Cancel; Up перемещает вверх на уровень (если есть куда, иначе кнопка задизаблена), Ok выбирает текущую папку. Кнопка Up имхо удобнее (и юзерфренднее, чем “..”, по хорошему юзер не знает, что эти точечки обозначают..). Строчка в списке “красивая” – рядом с именем папки иконка, цвет текствоки отличается для папок и файлов.

Для одного пункта списка создаем файл разметки:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    <ImageView
            android:id="@+id/icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingRight="2px"
            />
    <TextView
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/text"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="20sp"
            />
</LinearLayout>

Собственно можно усложнять и усложнять внешний вид, но мне этого “достаткол”. Разметка активити со списком и кнопками выглядит так:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
        >
    <ListView
            android:id="@+id/lstView"
            android:layout_alignParentTop="true"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            />
    <RelativeLayout
            android:orientation="horizontal"
            android:layout_alignParentBottom="true"
            android:layout_height="wrap_content"
            android:layout_width="fill_parent">
        <Button
                android:id="@+id/btnUp"
                android:layout_alignParentLeft="true"
                android:text="Up one level"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
        <Button
                android:id="@+id/btnOk"
                android:text="Select"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_toLeftOf="@+id/btnCancel"
                />
        <Button
                android:id="@+id/btnCancel"
                android:text="Cancel"
                android:layout_alignParentRight="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
    </RelativeLayout>
</RelativeLayout>

В сочетании со стилем android:theme="@android:style/Theme.Dialog" в манифесте внешне это будет выглядеть так:

device

package info.hamster.filelist;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.List;

public class FileListActivity extends Activity {

    private ListView fileList;
    private File currentDir;

    private CustomListAdapter listAdapter;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        listAdapter = new CustomListAdapter();

        fileList = (ListView) findViewById(R.id.lstView);
        fileList.setOnItemClickListener(new ListView.OnItemClickListener() {
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {

                CustomListItem item = (CustomListItem) listAdapter.getItem(i);

                String selectedFileString = currentDir + "/" + item.filename;
                File clickedFile = new File(selectedFileString);
                if ((clickedFile != null) && item.isDirectory)
                    browseTo(clickedFile);
            }
        });

        fileList.setAdapter(listAdapter);

        Button btnUp = (Button) findViewById(R.id.btnUp);
        btnUp.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View view) {
                if (!currentDir.toString().equals("/sdcard")) {
                    browseTo(currentDir.getParentFile());
                }
            }
        });

        Button btnOk = (Button)findViewById(R.id.btnOk);
        btnOk.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View view) {
                Bundle result = new Bundle();
                result.putString("dir", currentDir.toString());

                Intent resultIntent = new Intent();
                resultIntent.putExtras(result);
                setResult(RESULT_OK, resultIntent);
                finish();
            }
        });

        Button btnCancel = (Button)findViewById(R.id.btnCancel);
        btnCancel.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View view) {
                Log.d("TAG", "Cancel clicked");
                setResult(RESULT_CANCELED);
                finish();
            }
        });

        browseTo(new File("/sdcard"));
    }

    class ExtFilter implements FileFilter {
        public boolean accept(File file) {
            return file.getName().endsWith("png") || file.getName().endsWith("jpg") || file.isDirectory();
        }
    }

    private void browseTo(final File aDirectory) {

        if (aDirectory.isDirectory()) {
            this.currentDir = aDirectory;
            this.setTitle("Selected: " + this.currentDir);

            Button btnUp = (Button) findViewById(R.id.btnUp);
            btnUp.setEnabled(!aDirectory.toString().equals("/sdcard"));

            fill(aDirectory.listFiles(new ExtFilter()));
        }
    }

    private void fill(File[] files) {

        listAdapter.items.clear();
        if (files.length > 0)
            for (File file : files) {
                listAdapter.addItem(file.getName(), file.isDirectory());
            }
        else
            listAdapter.addItem("Empty directory", false);

    }

    public static class CustomListItem {
        public String filename;
        public boolean isDirectory;
    }

    private class CustomListAdapter extends BaseAdapter {

        private List<CustomListItem> items = new ArrayList<CustomListItem>();
        private LayoutInflater mInflater;

        public CustomListAdapter() {
            mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        public int getCount() {
            return items.size();
        }

        public Object getItem(int i) {
            return items.get(i);
        }

        public long getItemId(int i) {
            return i;
        }

        public View getView(int i, View view, ViewGroup viewGroup) {
            ViewHolder holder = null;

            if (view == null) {
                view = mInflater.inflate(R.layout.file_row, null);
                holder = new ViewHolder();
                holder.textView = (TextView) view.findViewById(R.id.text);
                holder.imageView = (ImageView) view.findViewById(R.id.icon);
                view.setTag(holder);
            } else {
                holder = (ViewHolder) view.getTag();
            }

            CustomListItem item = (CustomListItem) items.get(i);
            holder.textView.setText(item.filename);
            if (item.isDirectory) {
                holder.imageView.setImageResource(R.drawable.icon_folder);
                holder.textView.setTextColor(Color.WHITE);
            } else {
                holder.textView.setTextColor(Color.LTGRAY);
                holder.imageView.setImageResource(android.R.drawable.ic_menu_gallery);
            }

            return view;
        }

        public void addItem(final String filename, final boolean isDir) {

            CustomListItem item = new CustomListItem();
            item.filename = filename;
            item.isDirectory = isDir;

            items.add(item);

            notifyDataSetChanged();
        }

    }

    public static class ViewHolder {
        public TextView textView;
        public ImageView imageView;
    }
}

Значит тут (когда разобрался) в общем все не так и сложно: в OnCreate определяю, что делать с тапами - setResult и finish устанавливают результат показа активити и закрывают его, тап по пункту списка и кнопке Up вызывает заполнение списка новыми значениями

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

Класс CustomListItem (по сути просто запись, структура) хранит данные одного пункта списка – текствоку и признак “каталожности”; если надо – сюда можно добавлять и добавлять, была бы фантазия. Чтобы показывать в списке сложные данные нужен модифицированный адаптер – он должен реализовать методы предка + методы для добавления данных; у меня получилось, что он и хранит данные, и управляет их показом. Хранятся данные в списке List<CustomListItem> items, структура ViewHolder хранит указатели на контролы, которые заполняются данными методе getView.

В этом методе надо вывести в view данные; если view передается в метод пустым, он создается, используя разметку для строки списка, если view уже создан – для экономии он “заполняется данными нужного элемента списка (номер элемента передается в этот метод вместе с указателем на view

Ну собственно когда я тапаю по пункту списка – я получаю новое имя файла и вызываю browseTo; если это папка, то я получаю список файлов из нее (отфильтровывая только картинки и папки) и заполняю им список (если папка пуста или нет картинок – просто добавляю пункт “Пустой каталог”)

Когда я тапаю на Ok, надо вернуть результат: создается bundle, в который пихается что попало – в данном случае имя выбранного каталога; затем создается intent, в него пихается бандл с данными и интент передается с результатом. В обработчике onActivityResult (который вызывается, когда отрабатывает функция startActivityForResult берем интент, достаем из него бандл с данными, а из него – строку с именем папки и показываем (ну чтоб проконтролировать процесс)

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

Ну короче работает.. Вот тут архив с исходниками

Комментариев нет:

Отправить комментарий