App Theme Engine is a library that makes it easy for developers to implement a theme system in their apps, similar to what's seen in Cabinet and Impression.
Download the latest sample APK to check it out!
Add this in your root build.gradle
file (not your module build.gradle
file):
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
Add this to your module's build.gradle
file:
dependencies {
...
compile('com.github.afollestad:app-theme-engine:0.2.1') {
transitive = true
}
}
Before we go into details of how you can configure theme colors, you need to know how the theme engine is applied.
As seen in the sample project, you can have all Activities in your app extends ATEActivity
. This will do
all the heavy lifting for you, all that you have to worry about is theme configuration.
public class MyActivity extends ATEActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView() triggers the theme engine
setContentView(R.layout.my_layout);
}
}
If you were to change theme colors from a visible ATEActivity
, the changes are reflected automatically
if you use the apply()
methods discussed in the next two sections.
If you don't use ATEActivity
, there's a few things you have to do:
public class MyActivity extends AppCompatActivity {
private long updateTime = -1;
@Override
public void onCreate(Bundle savedInstanceState) {
ATE.preApply(this); // apply primary color to status bar, nav bar, and task description (recents)
super.onCreate(savedInstanceState);
setContentView(R.layout.my_layout); // call BEFORE apply()
updateTime = System.currentTimeMillis();
ATE.apply(this); // apply colors to other views in the Activity
}
@Override
protected void onResume() {
super.onResume();
// If values were applied/committed (from Config) since the Activity was created, recreate it now
if (ATE.didValuesChange(this, updateTime))
recreate();
}
}
You can also apply theming to views in a Fragment:
public class MyFragment extends Fragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ATE.apply(this);
}
}
If you have checkboxes or radio buttons in your Toolbar's overflow menu, you can tint them to your accent color:
public class MyActivity extends ATEActivity {
private Toolbar mToolbar;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);
mToolbar = (Toolbar) findViewById(R.id.my_toolbar);
setSupportActionbar(mToolbar); // ATE does not support toolbars that aren't set as action bars right now
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onMenuOpened(int featureId, Menu menu) {
// When the overflow menu opens, a tint is applied to the widget views inside
ATE.applyMenu(mToolbar);
return super.onMenuOpened(featureId, menu);
}
}
You could override onMenuOpened(int, Menu)
from any other type of Activity
too, not just ATEActivity
.
When working with lists, you have to apply the theme engine to individual views through your adapter.
For RecyclerViews:
public static class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
public MyAdapter() {
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View list = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
return new MyViewHolder(list);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
// Setup views
}
@Override
public int getItemCount() {
return 20;
}
public static class MyViewHolder extends RecyclerView.ViewHolder {
public MyViewHolder(View itemView) {
super(itemView);
// It's recommended you only apply the theme the first time the holder is created
ATE.apply(itemView.getContext(), itemView);
}
}
}
For ListViews:
public static class MyAdapter extends BaseAdapter {
@Override
public int getCount() {
return 20;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
// Only apply the first time the view is created
ATE.apply(convertView.getContext(), convertView);
}
return convertView;
}
}
ATE will automatically adapt when your Activity has a DrawerLayout
at its root. When coloredStatusBar()
is set to true, the primary dark theme color will be applied to the DrawerLayout
rather than directly to
the Window status bar. Thus, the status bar will be transparent when the drawer is open, and your theme
color when it's closed. You don't have to manually do anything.
If you use NavigationView
from the design support library, ATE will by default theme it. There are
navigation view theming configuration methods discussed in the next section. If your drawer uses a Fragment
or plain ListView
/RecyclerView
, you have to do what's discussed in the previous section.
By default, Android app themes are static. They cannot be changed dynamically after an APK is built. This library allows you to dynamically change theme colors at runtime.
All configuration options are persisted using SharedPreferences, meaning once you set them, you don't have to set them again unless you want the value to be changed from what it was previously.
Here are a few configuration methods that can be used:
ATE.config(this) // context
.primaryColor(color)
.primaryColorDark(color)
.accentColor(color)
.statusBarColor(color) // by default, is equal to whatever primaryColorDark is set to
.textColorPrimary(color)
.textColorSecondary(color)
.coloredStatusBar(true)
.coloredActionBar(true)
.coloredNavigationBar(false)
.autoGeneratePrimaryDark(true)
.navigationViewThemed(true)
.navigationViewSelectedIcon(color)
.navigationViewSelectedText(color)
.navigationViewNormalIcon(color)
.navigationViewNormalText(color)
.apply(this); // activity, fragment, or view
There's also color resource and color attribute variations of the color modifiers. For an example:
rather than using primaryColor(int)
, you could use primaryColorRes(int)
or primaryColorAttr(int)
in order to pass a value in the format R.color.resourceValue
or R.attr.attributeValue
.
If you want to setup a default configuration the first time your app is run, you can use code like this:
if (!ATE.config(this).isConfigured()) {
// Setup default options
}
Using the Config
class, you can retrieve your theme values (if you need to for any reason). For an example:
int primaryColor = Config.primaryColor(this);
If you want individual Activities to have different status bar colors, e.g. in an app that extracts
colors from an image using Palette to get theme colors, you can implement ATEStatusBarCustomizer
in the Activities which require it.
public class MyActivity extends AppCompatActivity implements ATEStatusBarCustomizer {
@ColorInt
@Override
public int getStatusBarColor() {
return Color.RED; // return whatever you want here
}
}
If you haven't used tags before, they can be applied to views directly from your XML layouts:
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="tag-value-here"
/>
The theme engine allows you to apply theme colors to any view using tags. You can even use multiple tags, separated by commas:
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="tag-one,tag-two,tag-three"
/>
Here's a list of available tag values:
You can change the background of any type of view.
bg_primary_color
- sets the background to the primary color.bg_primary_color_dark
- sets the background to the primary dark color.bg_accent_color
- sets the background to the accent color.bg_text_primary
- sets the background to the primary text color.bg_text_secondary
- sets the background to the secondary text color.
You can only change the text color of a view that extends TextView
, which includes Button
's.
text_primary_color
- sets the text color to the primary color.text_primary_color_dark
- sets the text color to the primary dark color.text_accent_color
- sets the text color to the accent color.text_primary
- sets the text color to the primary text color.text_secondary
- sets the text color to the secondary text color.
This should only really be needed on TextView'
s, it changes the color of links when TextViews are linkable.
text_link_primary_color
- sets the link text color to the primary color.text_link_primary_color_dark
- sets the link text color to the primary dark color.text_link_accent_color
- sets the link text color to the accent color.text_link_primary
- sets the link text color to the primary text color.text_link_secondary
- sets the link text color to the secondary text color.
You can tint CheckBox
's, RadioButton
's, ProgressBar
's, EditText
's, SeekBar
's, and ImageView
's.
tint_primary_color
- tints the view with the primary color.tint_primary_color_dark
- tints the view with the primary dark color.tint_accent_color
- tints the view with the accent color.tint_text_primary
- tints the view with the primary text color.tint_text_secondary
- tints the view with the secondary text color.
Seven views come stock with this library:
ATECheckBox
- tints itself to the accent color.ATERadioButton
- tints itself to the accent color.ATEEditText
- tints itself to the accent colorATEProgressBar
- tints itself to the accent color.ATESeekBar
- tints itself to the accent color.ATEPrimaryTextView
- sets its text color to the primary text color.ATESecondaryTextView
- sets its text color to the secondary text color.
All that they really do is set their own tag to one of the tag values in the previous section,
and then apply theming to themselves using the individual view apply()
method.