- Android: Changing app theme at runtime
- Changing the theme at runtime
- Possible issues
- Recursively change attributes on all your Views
- Possible issues
- There is a third option?
- What I recommend
- Do you know a better way of doing this?
- Введение в Android NDK
- Что такое Android NDK?
- Для чего используют NDK?
- Что такое JNI?
- Преимущества JNI
- Как устроен JNI
- Локальные и глобальные ссылки
- Обработка ошибок
- Примитивные типы JNI
- Ссылочные типы JNI
- Модифицированный UTF-8
- Функции JNI
- Пример использования функций JNI
- Потоки
- Первые шаги
- Android.mk
- Application.mk
- NDK-BUILDS
- Как собрать проект?
- Вызов нативных методов из Java кода
Android: Changing app theme at runtime
Jun 28, 2015 · 5 min read
Every so often, I see a question posted on StackOverflow which is effectively asks how to change the themes of an app at runtime. The use case is often that there is a setting, button or check box which can switch between different colour themes or between something like day and night mode.
Every time such a requirement comes up, the fir s t thing a quick Google search shows is that it’s not possible to change themes at runtime. That being said, it is possible to change the theme of an Activity, however only in the`onCreate` method and only before `super` is called.
This is problematic because it’s hard to provide a seamless experience to the user if you have to restart the app or Activity in order to change the theme. So our second option is to recursively loop through all of our views and set their attributes each time an Activity or Fragment is created. This way, when the theme is changed, you can loop through all the Views again and change the attributes to reflect the new theme. Neither of these options is ideal, you may even want to consider a hybrid of these two approaches. I’ve provided an example implementation of each of these methods below.
Changing the theme at runtime
As mentioned before, it’s only possible to change the theme of an Activity in the `onCreate` method and that to only before `super` has been called. Changing the theme is fairly straight forward, something like below should do it:
This is pretty straight forward, however this works when an activity is first created and has no effect on the current open Activity or any backgrounded Activities. In order to affect change on the current Activity, we’ll have save the state of the current Activity and relaunch the activity, in order to make this experience seamless for the user, you have 2 options, either remove all transition animations for Activities or change them to provide a nice fade in effect. The result of this approach is shown in the video below.
As you can see, the approach produces a pretty nice result. If you don’t want a fade in effect, remove all animations for Activity transition and you should have a sudden change.
The code to achieve this is in my gist “Transition themes”.
Possible issues
- In order to achieve theme change in this manner, you have to make sure that all your View inherit attributes that matter from the theme and do not in-line any attributes that matter like background colour or text colour.
- Saving your Activity state and relaunching it may not be as smooth as in my example above. This depends a lot of how heavy your Activity and it’s layouts are. Some elements may need to be reloaded.
- Any Activities that are already open in the background will not have the theme change applied to it when you go back to them. The easiest solution to this is to close all the backgrounded Activities, or else, you’ll have to save their state, close them and relaunch them in `onStart` or `onResume`.
Recursively change attributes on all your Views
As much as we hope that the theme can contain all our formatting, we invariably need to override a text colour or background colour in-line in our layout or an in a style attribute and this needs to be changed programmatically. In this scenario, you would likely have to check the appropriate Views or all Views to see if they are consistent with your set theme. If you know which Views are likely to be affected and can check them directly, nothing could be better. If not, you will have to loop through all the View in your layout and check them. The code to do this depends heavily on your project and it’s requirements, however, the skeleton code for checking all your Views be something like this:
Possible issues
- Depending on how complex your screens are, your code for checking each View can become quite complex. An alternate solution can be to set the Views theme related attributes when we build our Activity, Fragment or Layout. This will still add to the complexity of your code.
- There is a time and performance cost to doing this for each layout.
There is a third option?
You could bundle duplicate layouts for each of your themes where the only difference between each layout is that the style or in-line style related attributes are different. Then in your code, depending on the selected theme you inflate or set the appropriate layout. This approach while very simple, however it has the same issues as the first option.
What I recommend
If this is a requirement for your app, I recommend you research what is possible before you try any of these approaches. If all you want to do is change some text colour and the colour of the Toolbar and tabs, this is possible without having to change the theme. I would take a look at the Design Support Library.
If you are going to do down one of the routes I have talked about above, I would recommend not getting too attached to any one approach and to combine all three approaches above. Find the best fit for your particular situation.
Also, if you’re going to need to change the colour of your drawable assets, my article on how to change the colour of drawable assets may help.
Do you know a better way of doing this?
I’m honestly asking the readers, if there are any out there, to chime in and tell me if there is a better way to handle runtime theme changes. It’s a topic I have researched and Google’d, however, I’m just not happy with what I’ve found so far. If you have a better approach or some advice on the matter, I’d love to hear it.
For more Android development article or follow me on LinkedIn, Twitter or Google+.
Источник
Введение в Android NDK
Для разработки приложений под ОС Android, Google предоставляет два пакета разработки: SDK и NDK. Про SDK существует много статей, книжек, а так же хорошие guidelines от Google. Но про NDK даже сам Google мало что пишет. А из стоящих книг я бы выделил только одну, Cinar O. — Pro Android C++ with the NDK – 2012.
Эта статья ориентирована на тех, кто ещё не знаком (или мало знаком) с Android NDK и хотел бы укрепить свои знания. Внимание я уделю JNI, так как мне кажется начинать нужно именно с этого интерфейса. Так же, в конце рассмотрим небольшой пример с двумя функциями записи и чтения файла. Кто не любит много текста, тот может посмотреть видео версию.
Что такое Android NDK?
Android NDK (native development kit) – это набор инструментов, которые позволяют реализовать часть вашего приложения используя такие языки как С/С++.
Для чего используют NDK?
Google рекомендует прибегать к использованию NDK только в редчайших случаях. Зачастую это такие случаи:
- Нужно увеличить производительность (например, сортировка большого объема данных);
- Использовать стороннюю библиотеку. Например, много уже чего написано на С/С++ языках и нужно просто заиспользовать существующий материал. Пример таких библиотек, как, Ffmpeg, OpenCV;
- Программирование на низком уровне (например, всё что выходит за рамки Dalvik);
Что такое JNI?
Java Native Interface – стандартный механизм для запуска кода, под управлением виртуальной машины Java, который написан на языках С/С++ или Assembler, и скомпонован в виде динамических библиотек, позволяет не использовать статическое связывание. Это даёт возможность вызова функции С/С++ из программы на Java, и наоборот.
Преимущества JNI
Основное преимущество перед аналогами (Netscape Java Runtime Interface или Microsoft’s Raw Native Interface and COM/Java Interface) является то что JNI изначально разрабатывался для обеспечения двоичной совместимости, для совместимости приложений, написанных на JNI, для любых виртуальных машин Java на конкретной платформе (когда я говорю о JNI, то я не привязываюсь к Dalvik машине, потому как JNI был написан Oracle для JVM который подходит для всех Java виртуальных машин). Поэтому скомпилированный код на С/С++ будет выполнятся в не зависимости от платформы. Более ранние версии не позволяли реализовывать двоичную совместимость.
Двоичная совместимость или же бинарная совместимость – вид совместимости программ, позволяющий программе работать в различных средах без изменения её исполняемых файлов.
Как устроен JNI
JNI таблица, организована как таблица виртуальных функций в С++. VM может работать с несколькими такими таблицами. Например, одна будет для отладки, вторая для использования. Указатель на JNI интерфейс действителен только в текущем потоке. Это значит, что указатель не может гулять с одного потока в другой. Но нативные методы могут быть вызваны из разных потоков. Пример:
- *env – указатель на интерфейс;
- оbj – ссылка на объект в котором описан нативный метод;
- i and s – передаваемые аргументы;
Примитивные типы копируются между VM и нативным кодом, а объекты передаются по ссылке. VM обязана отслеживать все ссылки которые передаются в нативный код. Все переданные ссылки в нативный код не могут быть освобождены GC. Но нативный код в свою очередь должен информировать VM о том что ему больше не нужны ссылки на переданные объекты.
Локальные и глобальные ссылки
JNI делит ссылки на три типа: локальные, глобальные и слабые глобальные ссылки. Локальные действительны пока не завершиться метод. Все Java объекты которые возвращает функции JNI являются локальными. Программист должен надеется на то что VM сама подчистит все локальные ссылки. Локальные ссылки доступны лишь в том потоке в котором были созданы. Однако если есть необходимость то их можно освобождать сразу методом JNI интерфейса DeleteLocalRef:
Глобальные ссылки остаются пока они явно не будут освобождены. Что бы зарегистрировать глобальную ссылку следует вызвать метод NewGlobalRef. Если же глобальная ссылка уже не нужна, то её можно удалить методом DeleteGlobalRef:
Обработка ошибок
JNI не проверяет ошибки такие как NullPointerException, IllegalArgumentException. Причины:
- снижение производительности;
- в большинстве функций C библиотек очень и очень трудно защитится от ошибок.
JNI позволяет использовать Java Exception. Большинство JNI функций возвращают код ошибок а не сам Exception, и поэтому приходится обрабатывать сам код, а в Java уже выбрасывать Exception. В JNI следует проверять код ошибки вызываемых функций и после них следует вызвать ExceptionOccurred(), которая в свою очередь возвращает объект ошибки:
Например, некоторые функции JNI доступа к массивам не возвращают ошибки, но могут вызвать исключения ArrayIndexOutOfBoundsException или ArrayStoreException.
Примитивные типы JNI
В JNI существуют свои примитивные и ссылочные типы данных.
Java Type | Native Type | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
Ссылочные типы JNI
Модифицированный UTF-8
JNI использует модифицированную кодировку UTF-8 для представления строк. Java в свою очередь использует UTF-16. UTF-8 в основном используется в С, потому что он кодирует \u0000 в 0xc0, вместо привычной 0x00. Изменённые строки кодируются так, что последовательность символов, которые содержат только ненулевой ASCII символы могут быть представлены с использованием только одного байта.
Функции JNI
Интерфейс JNI содержит в себе не только собственный набор данных, но и свои собственные функции. На их рассмотрение уйдёт много времени, так как их не один десяток. Ознакомится с ними вы сможете в официальной документации.
Пример использования функций JNI
Небольшой пример, что бы вы усвоили пройденный материал:
Разберём построчно:
- JavaVM – предоставляет интерфейс для вызова функций, которые позволяют создавать и уничтожать JavaVM;
- JNIEnv – обеспечивает большинство функций JNI;
- JavaVMInitArgs – аргументы для JavaVM;
- JavaVMOption – опции для JavaVM;
Метод JNI_CreateJavaVM() инициализирует JavaVM и возвращает на неё указатель. Метод JNI_DestroyJavaVM() выгружает созданную JavaVM.
Потоки
Всеми потоками в Linux управляет ядро, но они могут быть прикреплены к JavaVM функциями AttachCurrentThread и AttachCurrentThreadAsDaemon. Пока поток не присоединён он не имеет доступа к JNIEnv. Важно, Android не приостанавливает потоки которые были созданы JNI, даже если срабатывает GC. Но перед тем как поток завершиться он должен вызвать метод DetachCurrentThread что бы отсоединиться от JavaVM.
Первые шаги
Структура проекта у вас должна выглядеть следующим образом:
Как мы видим из рисунка 3, весь нативный код находится в папке jni. После сборки проекта, в папке libs создастся четыре папки под каждую архитектуру процессора, в которой будет лежать ваша нативная библиотека (количество папок зависит от количество выбранных архитектур).
Для того, чтобы создать нативный проект, нужно создать обычный Android проект и проделать следующие шаги:
- В корне проекта нужно создать папку jni, в которую поместить исходники нативного кода;
- Создать файл Android.mk, который будет собирать проект;
- Создать файл Application.mk, в котором описываются детали сборки. Он не является обязательным условием, но позволяет гибко настроить сборку;
- Создать файл ndk-build, который будет запускать процесс сборки (тоже не является обязательным).
Android.mk
Как упоминалось уже выше, это make файл для сборки нативного проекта. Android.mk позволяет группировать ваш код в модули. Модули могут быть как статические библиотеки (static library, только они будут скопированные в ваш проект, в папку libs), разделяемые библиотеки (shared library), автономный исполняемый файл (standalone executable).
Пример минимальной конфигурации:
Рассмотрим детально:
- LOCAL_PATH := $(call my-dir) – функция call my-dir возвращает путь папки в которой вызывается файл;
- include $(CLEAR_VARS) – очищает переменные которые использовались до этого кроме LOCAL_PATH. Это необходимо так как все переменные являются глобальными, потому что сборка происходит в контексте одного GNU Make;
- LOCAL_MODULE – имя выходного модуля. В нашем примере имя выходной библиотеки установлено как NDKBegining, но после сборки в папке libs создадутся библиотеки с именами libNDKBegining. Android добавляет к названию префикс lib, но в java коде при подключении вы должны указывать название библиотеки без префикса (то есть названия должны совпадать с установленными в make файлах);
- LOCAL_SRC_FILES – перечисление исходных файлов из которых следует создать сборку;
- include $(BUILD_SHARED_LIBRARY) – указывает тип выходного модуля.
В Android.mk можно определить свои переменные, но они не должны иметь такой синтаксис: LOCAL_, PRIVATE_, NDK_, APP_, my-dir. Google, рекомендует называть свои переменные, как MY_. Например:
Application.mk
NDK-BUILDS
Ndk-build из себя представляет обёртку GNU Make. После 4-й версии ввели флаги для ndk-build:
- clean – очищает все сгенеренные бинарные файлы;
- NDK_DEBUG=1 – генерирует отладочный код;
- NDK_LOG=1 – показывает лог сообщений (используется для отладки);
- NDK_HOST_32BIT=1 – Android имеет средства для поддержки 64-х битных версий утилит (например NDK_PATH\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64 и т.д.);
- NDK_APPLICATION_MK — указывается путь к Application.mk.
В 5-й версии NDK был введён такой флаг как NDK_DEBUG. Если он установлен в 1 то создаётся отладочная версия. Если флаг не установлен то ndk-build по умолчанию проверяет стоит ли атрибут android:debuggable=«true» в AndroidManifest.xml. Если вы используете ndk выше 8-й версии, то Google не рекомендует использовать атрибут android:debuggable в AndroidManifest.xml (потому что если вы используете «ant debug» или строите отладочную версию с помощью ADT плагина то они автоматически добавляют флаг NDK_DEBUG=1).
По умолчанию устанавливается поддержка 64-х разрядной версии утилит, но вы можете принудительно собрать только для 32-х установив флаг NDK_HOST_32BIT=1. Google, рекомендует всё же использовать 64-х разрядность утилит для повышения производительности больших программ.
Как собрать проект?
Раньше это было мучением. Нужно было установить CDT плагин, скачать компилятор cygwin или mingw. Скачать Android NDK. Подключить это всё в настройках Eclipse. И как на зло это всё оказывалось не рабочим. Я первый раз когда столкнулся с Android NDK, то настраивал это всё 3 дня (а проблема оказалось в том что в cygwin нужно было дать разрешение 777 на папку проекта).
Сейчас с этим всё намного проще. Идёте по этой ссылке. Качаете Eclipse ADT Bundle в котором уже есть всё то что необходимо для сборки.
Вызов нативных методов из Java кода
Для того что бы использовать нативный код из Java вам сперва следует определить нативные методы в Java классе. Например:
Перед методом следует поставить зарезервированное слово «native». Таким образом компилятор знает, что это точка входа в JNI. Эти методы нам нужно реализовать в С/С++ файле. Так же Google рекомендует начинать именовать методы со слова nativeХ, где Х – реальное название метода. Но перед тем как реализовывать эти методы вручную, следует сгенерировать header файл. Это можно сделать вручную, но можно использовать утилиту javah, которая находится в jdk. Но пойдём дальше и не будет использовать её через консоль, а будем это делать при помощи стандартных средств Eclipse.
Теперь можете запускать. В директории bin/classes будут лежать ваши header файлы.
Далее копируем эти файлы в jni директорию нашего нативного проекта. Вызываем контекстное меню проекта и выбираем пункт Android Tools – Add Native Library. Это позволит нам использовать jni.h функции. Дальше вы уже можете создавать cpp файл (иногда Eclipse его создаёт по умолчанию) и писать тела методов, которые уже описаны в header файле.
Пример кода я не стал добавлять в статью, чтобы не растягивать её. Пример вы можете посмотреть/скачать с github.
Источник