Pull to refresh

Android: Quick control menu

Development for Android *
Sandbox
Всем желающим использовать меню из браузера и новой камеры

camera_samplebrowser_sample

добро пожаловать под кат.



Предыстория

Во время работы над одним из приложений встал вопрос как делать меню для пользователя. Вдоволь наигравшись с приложением Browser для ICS было решено использовать его опциональный компонент PieMenu. В другом проекте возникла необходимость в меню, которое не только симпатично появляется, но и умеет это делать в произвольном месте.
Для меня остается большой загадкой почему данные меню не были добавлены в API: довольно большому числу людей данные меню нравятся, да и разработчикам не было бы необходимости изобретать велосипед.

Исходники

Итак, путь мой лежал на AOSP(Android Open Source Project). Поиск там исходников данных приложений увенчался успехом. Обе версии меню были взяты из master branch'ей.

Модификация бокового меню

multi-level pieДалее были убраны все привязки к приложению: поля, методы, listener'ы и т.п. (а код действительно очень связан с компонентами приложений, т.е. банально взять и объявить объект не получится). Для удобства использования бокового меню добавил возможность устанавливать отдельные Listener'ы на разные пункты меню, добавление Item'ов из List'а. В исходной реализации совершенно не работал параметр level в конструкторе. Он отвечает за номер кольца в меню, где 1ое кольцо с минимальным радиусом, а с ростом level'а растет радиус. После изучения кода выяснилось, что Path, который рисует задний фон для иконки, переприсваивался для каждого нового кольца в методе отрисовки, что не давало возможности использовать multi-level меню. Данный недостаток был устранен.
Данное меню поддерживает отображение подменю. Со стороны кода это выглядит как добавление Item'ов к другому Item'у.
Важный момент: выставляйте количество подменю для каждого item'а совпадающее с каким либо кольцом. Например, у вас 3 item'а в первом кольце и 2 во втором. Тогда на подменю item'а 1го кольца можно поставить либо 2 элемента (т.к. при развертывании останется только 2 позиции), либо 2 элемента 2го кольца (т.к. их можно полностью заместить). Изначально код при нарушении данного условия уходил к апостолу Петру и жаловался, что его заставляют делать страшные вещи. Однако сейчас вы можете поэкспериментировать с тем как будет вести себя код не опасаясь Exception'ов.

PieControl

browser_sampleДанный класс позволяет реализовать боковое меню.
Объявим наследника данного класса и переопределим методы populateMenu(). Также советую добавить setListeners() для удобной установки Listener'ов нажатий. Элемент PieItem создаем с помощью makeItem(). В качестве параметров указываем ресурс иконки и level (номер кольца). Затем создаем объект нашего нового класса, привязываем его к frame'у через attachToContainer() и устанавливаем Listener'ы вызывая метод setListeners().

Пример наследника:
private class TestMenu extends PieControl {
        List<PieItem> plus_sub;
        List<PieItem> minus_sub;

        public TestMenu(Activity activity) {
            super(activity);
        }

        protected void populateMenu() {
            PieItem plus = makeItem(android.R.drawable.ic_input_add,1);
            {
                plus_sub = new ArrayList<PieItem>(2);
                plus_sub.add(makeItem(android.R.drawable.ic_input_add,2));
                plus_sub.add(makeItem(android.R.drawable.ic_input_add, 2));
                plus.addItems(plus_sub);
            }
            PieItem minus = makeItem(android.R.drawable.ic_menu_preferences,1);
            {
                minus_sub = new ArrayList<PieItem>(2);
                minus_sub.add(makeItem(android.R.drawable.ic_menu_preferences,1));
                minus_sub.add(makeItem(android.R.drawable.ic_menu_preferences, 1));
                minus.addItems(minus_sub);
            }
             PieItem close = makeItem(android.R.drawable.ic_menu_close_clear_cancel,1);
            mPie.addItem(plus);
            mPie.addItem(minus);
            mPie.addItem(close);

            PieItem level2_0 = makeItem(android.R.drawable.ic_menu_report_image, 2);

            mPie.addItem(level2_0);

            PieItem level2_1 = makeItem(android.R.drawable.ic_media_next, 2);

            mPie.addItem(level2_1);
        }

        public void setListeners() {
            this.setClickListener(plus_sub.get(0),new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(getApplicationContext(),"Clicked plus 1", Toast.LENGTH_SHORT).show();
                }
            });
            this.setClickListener(plus_sub.get(1), new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(getApplicationContext(),"Clicked plus 2", Toast.LENGTH_SHORT).show();
                }
            });
            this.setClickListener(minus_sub.get(0), new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(getApplicationContext(),"Clicked minus 1", Toast.LENGTH_SHORT).show();
                }
            });
            this.setClickListener(minus_sub.get(1), new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Toast.makeText(getApplicationContext(),"Clicked minus 2", Toast.LENGTH_SHORT).show();
                }
            });
            this.setClickListener(close, new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    finish();
                }
            });
        }
    }


Пример объявления меню:

        TestMenu testMenu = new TestMenu(this);
        testMenu.attachToContainer(some_container);
        testMenu.setListeners();



PieRenderer

camera_sampleДанный класс позволяет реализовать круговое меню из приложения Camera. Использовать его, по моему мнению, гораздо проще, чем PieControl. Концептуально это меню отличается от бокового: привязывать его можно не к любым наследникам ViewGroup, а лишь к специальному RenderOverlay (который на самом деле является обычным FrameLayout с небольшим набором дополнительных методов, но это уже inner workings). К RenderOverlay можно привязать объекты типа Renderer, наследником которого является PieRenderer. Он то нам и нужен для отрисовки меню. Так же нам необходим класс PieController для добавления элементов в меню. Итак, приступим:
Создаем объект PieRenderer, объект PieController, элементы меню с помощью метода makeItem в PieController. Добавляем элементы меню в PieRenderer через addItem. Затем создаем объект RenderOverlay (либо находим через findViewById, если вы любите определять все в xml). Добавляем PieRendere к RenderOverlay через addRenderer. Теперь последний штрих: в onTouchEvent пошлите событие к обработчику PieRenderer

Пример кода Activity:

public class MainActivity extends Activity {
    private static float FLOAT_PI_DIVIDED_BY_TWO = (float) Math.PI / 2;
    private final static float sweep = FLOAT_PI_DIVIDED_BY_TWO / 2;
    private PieRenderer pieRenderer;

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

        pieRenderer = new PieRenderer(getApplicationContext());
        PieController pieController = new PieController(this, pieRenderer);

        RenderOverlay renderOverlay = (RenderOverlay) findViewById(R.id.render_overlay);

        PieItem item0 = pieController.makeItem(android.R.drawable.arrow_up_float);
        item0.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO, sweep);
        item0.setOnClickListener(new PieItem.OnClickListener() {
            @Override
            public void onClick(PieItem item) {
                Toast.makeText(getApplicationContext(), "some cmd", Toast.LENGTH_SHORT).show();
            }
        });

        PieItem item1 = pieController.makeItem(android.R.drawable.arrow_up_float);
        item1.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
        item1.setOnClickListener(new PieItem.OnClickListener() {
            @Override
            public void onClick(PieItem item) {
                Toast.makeText(getApplicationContext(), "some cmd 2", Toast.LENGTH_SHORT).show();
            }
        });

        PieItem item7 = pieController.makeItem(android.R.drawable.arrow_up_float);
        item7.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep);
        item7.setOnClickListener(new PieItem.OnClickListener() {
            @Override
            public void onClick(PieItem item) {
                Toast.makeText(getApplicationContext(), "some cmd 7", Toast.LENGTH_SHORT).show();
            }
        });

        pieRenderer.addItem(item0);
        pieRenderer.addItem(item1);
        pieRenderer.addItem(item7);

        PieItem item0_0 = pieController.makeItem(android.R.drawable.ic_menu_add);
        item0_0.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO, sweep);
        item0_0.setOnClickListener(new PieItem.OnClickListener() {
            @Override
            public void onClick(PieItem item) {
                Toast.makeText(getApplicationContext(), "some cmd", Toast.LENGTH_SHORT).show();
            }
        });

        PieItem item0_6 = pieController.makeItem(android.R.drawable.ic_menu_add);
        item0_6.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO + sweep, sweep);
        item0_6.setOnClickListener(new PieItem.OnClickListener() {
            @Override
            public void onClick(PieItem item) {
                Toast.makeText(getApplicationContext(), "some cmd 2", Toast.LENGTH_SHORT).show();
            }
        });

        PieItem item0_7 = pieController.makeItem(android.R.drawable.ic_menu_add);
        item0_7.setFixedSlice(FLOAT_PI_DIVIDED_BY_TWO - sweep, sweep);
        item0_7.setOnClickListener(new PieItem.OnClickListener() {
            @Override
            public void onClick(PieItem item) {
                Toast.makeText(getApplicationContext(), "some cmd 7", Toast.LENGTH_SHORT).show();
            }
        });

        item0.addItem(item0_0);
        item0.addItem(item0_6);
        item0.addItem(item0_7);

        renderOverlay.addRenderer(pieRenderer);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        pieRenderer.onTouchEvent(event);
        return super.onTouchEvent(event);
    }
}


Послесловие

Исходники получившейся библиотеки можно стянуть отсюда.
Запускается на Android 2.2.1 и выше (теоретически работает на Android 1.0 и выше).

Спасибо за внимание и удачного Вам дня!

Update:
Спасибо Prototik за подсказку про портированные классы Animation. Теперь меню работает и на старых API: JakeWharton указывает минимальный API 1.0, но мне удалось проверить на 2.2.1 и выше.
Tags:
Hubs:
Total votes 42: ↑41 and ↓1 +40
Views 20K
Comments 19
Comments Comments 19