Как отделить тему от приложения

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

Существуют встроенные в платформу средства для этих целей (темы, стили), однако они не предоставляют внятного механизма изменения интерфейса приложения без изменения кода самого приложения.

Мною предлагается технология, позволяющая динамически менять внешний вид Android приложения путем установки новых «тем», которые могут быть скачаны отдельно от приложения. Описанная в статье разработка выполнялась в качестве пилотного проекта в департаменте мобильных приложений компании «Мера-НН» (www.meranetworks.com), где и работает автор статьи.

Библиотека


Рассматриваемый механизм предусматривает распространение тем в виде отдельных .apk-файлов, которые могут быть скачаны с сайта разработчика и установлены на устройство как обычное Android-приложение. Возможна смена тем как для базовых элементов платформы так и для вновь созданных (custom) UI элементов.

Идея базируется на том, что Android API позволяет из одного приложения получить доступ к ресурсам другого приложения.

Пример кода для доступа к ресурам другого приложения:

PackageManager pm = context.getPackageManager();
Resources res = pm.getResourcesForApplication ("package name");
Resources.Theme rstheme = res.newTheme();

здесь package name – произвольный пакет, установленный на устройстве.

Смена темы для отдельной активности может быть произведена только непосредственно перед ее созданием. Чтобы не делать это каждый раз мною был создан класс BaseActivity в котором переопределен метод onCreate(). Также, если мы собираемся использовать ресурсы другого приложения, необходимо переопределить методы которые используются для получения доступа к ресурсам и темам из этих ресурсов.

Исходный код класса BaseActivity приведен ниже.

package com.mera.detachedthemeslib;

import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;

public abstract class BaseActivity extends Activity {

	@Override
	public void onCreate(Bundle savedInstanceState) {
		ActivityManager.setThemeForActivity(this);
		super.onCreate(savedInstanceState);
	}

	@Override
	public Resources getResources() {
		return ActivityManager.getResourcesForActivity(this, super.getResources());
	}

	@Override
	public Resources.Theme getTheme() {
		return ActivityManager.getThemeForActivity(this, super.getTheme());
	}
}

Для того что бы управлять текущей конфигурацией приложения был создан класс ThemeManager.
Он реализует следующую функциональность:
  • позволяет получать список доступных тем;

static List<Theme> getThemes(Context ctx, ThemesConfiguration cfg)

  • позволяет производить переключение между темами.

static void setTheme(Context ctx, Theme theme)

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

Данные классы (BaseActivity и ThemeManager), а также набор вспомогательных классов, были объединены в библиотеку, которая доступна на github.

Разработка приложений с использованием библиотеки:


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

Во-первых, для того, чтобы разрабатываемое приложение позволяло производить динамическую смену тем, необходимо чтобы все Activity данного приложения были унаследованы от BaseActivity (BaseListActivity, BasePreferenceActivity).

Во-вторых, приложение должно содержать тему с именем «MainTheme».
Пример из разработанного приложения:

<style name="MainTheme" parent="android:Theme">
        <item name="custButtonStyle">@android:style/Widget.Button</item>
</style>

При наличии других стандартных тем, встроенных в приложение, и при необходимости cделать их доступными для пользователей, их следует указать при обращении к ThemeManager. Пример того, как это делается приведен ниже, при добавлении темы «Green».

Имеются также ограничения для дополнительного приложения предоставляющего собой отдельную тему:
  • Имя пакета для него должно начинаться также как и имя для основного приложения.
  • Приложение-тема должно содержать тему с именем “DetachedTheme”.

Ниже приведен пример кода для приложения используещего отделенные темы:

Главный экран приложения:

package com.mera.detachedthemesapp;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;

import com.mera.detachedthemeslib.BaseActivity;

public class MainActivity extends BaseActivity {
	    
	@Override
	public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        String[] items = new String[] {"One", "Two", "Three"};
        Spinner spinner = (Spinner) findViewById(R.id.spinner1);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                    android.R.layout.simple_spinner_item, items);
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spinner.setAdapter(adapter);

        this.findViewById(R.id.switch_button).setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View arg0) {
				startActivity(new Intent(MainActivity.this, SwitchThemeActivity.class));
			}
		});
	}
}

Экран со списком тем для переключение между ними:

package com.mera.detachedthemesapp;

import java.util.List;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import com.mera.detachedthemeslib.BaseListActivity;
import com.mera.detachedthemeslib.ThemesConfiguration;
import com.mera.detachedthemeslib.ThemeManager;

public class SwitchThemeActivity extends BaseListActivity {

    private LayoutInflater mInflater;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        List<ThemeManager.Theme> themes = ThemeManager.getThemes(this,
                new ThemesConfiguration()
                        .addInnerTheme(R.style.CustomTheme2, "Green"));

        ArrayAdapter<ThemeManager.Theme> adapter = new ArrayAdapter<ThemeManager.Theme>(
                this, android.R.layout.simple_list_item_1, themes) {

            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                View row;

                if (null == convertView) {
                    row = mInflater.inflate(
                            android.R.layout.simple_list_item_1, null);
                } else {
                    row = convertView;
                }

                TextView tv = (TextView) row.findViewById(android.R.id.text1);
                tv.setText(((ThemeManager.Theme) getItem(position)).mTitle);
                return row;
            }

        };

        getListView().setAdapter(adapter);
        getListView().setOnItemClickListener(new OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id) {
                ThemeManager.Theme theme = (ThemeManager.Theme) getListView()
                        .getAdapter().getItem(position);

                ThemeManager.setTheme(SwitchThemeActivity.this, theme);
                Intent intent = new Intent();
                intent.setClass(SwitchThemeActivity.this, MainActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                SwitchThemeActivity.this.startActivity(intent);
            }
        });

    }
}

Как это выглядит...


Разработанное приложение позволяет производить смену внешнего вида приложения используя две встроенные темы.

Так же приложение позволяет установить и использовать новую тему, которая была скачана и установлена отдельно как обычное Android-приложение.

Код библиотеки и примера приложения с отделенной темой доступен на github.
DetachedThemesLib — библиотека
DetachedThemesApp — приложение
DetachedThemesTheme — отделенная тема
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 20

  • НЛО прилетело и опубликовало эту надпись здесь
      0
      Извините, но ваш вопрос мне кажется не полным. Что с нативным внешним видом не так?
      • НЛО прилетело и опубликовало эту надпись здесь
          +1
          Ну приложение может же быть например ланчером, кастомизация вида которого как раз очень важна пользователю…
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              ну почему же? в играх тоже довольно полезно.
      +2
      Я согласен, что не-гайдлайновые интерфейсы это безусловно не есть хорошо. Но мне кажется, что существующие гайдлайны, например такие, оставляют достаточный простор для творчества и применения тем.
      И да, с к сожалению ни кто не застрахован от того что его технология будет не правильно интерпретирована или не правильно применена горе-разработчиками.
        0
        а чем вас обычный механизм тем/стилей не устроил?
        я просто не могу понять, зачем устанавливать отдельный .apk и пользоваться библиотекой, если можно все описать в styles.xml файле?
        разве что для загрузки новых тем, не встроенных изначально в приложение
          0
          Да, именно для загрузки новых тем, не встроенных изначально в приложение.
          0
          Я «обожаю» такой подход: отнаследовались от Activity и радуемся.
          Во-первых, вы забыли в своей библиотеке о FragmentActivity, а, во-вторых, это не правильно: composition over inheritance (т.е. в переводе на русский «композиция вместо наследования»).

          В качестве примера приведу такие замечательные библиотеки как SherlockActionBar и Roboguice. В обеих авторы наследуются от Activity, ListActivity и т.д., но вот беда — как их использовать вместе? Правильно, написать ещё одну библиотеку RoboSherlock, где уже классы RoboSherlockActivity наследуется от Activity и т.д.
            +1
            Кстати, я бы не назвал хорошей практикой ловлю NullPointerException (там действительно в недрах Андроида выкидывается? Или вы просто поленились добавить проверку?)
              0
              А про какой NullPointerException вы говорите? Действительно возможно просто не хватает проверки.
                0
                ThemeManager, строка 22
                Там же, строка 114
                  0
                  NullPointerException в недрах Андроида не выкидывается. Скорее всего проверка у меня осталась еще с момента разработки поскольку сейчас вообще ни какого NullPointerException там быть не может.)
              0
              Я тоже не особо люблю такое наследование…
              Надо как-то так писать:
              class MyActivity { public void onCreate() { super.onCreate(); onCreate(this); } public static void onCreate(Activity activity) { // всякие хорошие вещи... } }
                0
                чорт, с переводами строки тут проблемы…
                  0
                  Спасибо за идею. Думаю с учетом вашего комментария обновленная BaseActivity будет выглядеть следующим образом:

                  package com.mera.detachedthemeslib;
                  
                  import android.app.Activity;
                  import android.content.res.Resources;
                  import android.os.Bundle;
                  
                  public abstract class BaseActivity extends Activity {
                  
                  	@Override
                  	public void onCreate(Bundle savedInstanceState) {
                  		onCreate(this);
                  		super.onCreate(savedInstanceState);
                  	}
                  	
                  	@Override
                  	public Resources getResources() {
                  		return getResources(this, super.getResources());
                  	}
                  
                  	@Override
                  	public Resources.Theme getTheme() {
                  		return getTheme(this, super.getTheme());
                  	}
                  	
                  	public static void onCreate(Activity activity) {
                  		ActivityManager.setThemeForActivity(activity);
                  	}
                  	
                  	public static Resources getResources(Activity activity, Resources superResources) {
                  		return ActivityManager.getResourcesForActivity(activity, superResources);
                  	}
                  	
                  	public static Resources.Theme getTheme(Activity activity, Resources.Theme superTheme) {
                  		return ActivityManager.getThemeForActivity(activity, superTheme);
                  	}
                  }
                  
                    0
                    Выделите лучше в отдельный класс и используйте как поле уже в клиентском Activity:
                    interface ThemeHelper {
                    
                        void onCreate(@NotNull Activity activity);
                    
                        @NotNull
                        Resources getResources(@NotNull Activity activity);
                    
                    }
                    


                    Использование:

                    class MyActivity extends RoboSherlockActivity {
                    
                        @NotNull
                        private final ThemeHelper themeHelper = new ThemeHelper();
                    
                        @Override
                        void onCreate(Bundle savedInstanceState) {
                            themeHelper.onCreate(this);
                    
                            super.onCreate(savedInstanceState);
                    
                            // ...
                        }
                    
                    }
                    

                      0
                      Я согласен что так будет лучше. Сделаю соответственно так при обновлении кода.
                  +1
                  Спасибо за замечание. Попробую исправить реализацию что бы наследование от BaseActivity (BaseListActivity, BasePreferenceActivity, ...) не было обязательным условием. Я думаю что для этого мне достаточно сделать класс ActivityManager публично доступным.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое