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.
publicclassMyActivityextendsATEActivity{@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); // setContentView() triggers the theme enginesetContentView(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:
publicclassMyActivityextendsAppCompatActivity{privatelongupdateTime = -1; @OverridepublicvoidonCreate(BundlesavedInstanceState){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 } @OverrideprotectedvoidonResume(){super.onResume(); // If values were applied/committed (from Config) since the Activity was created, recreate it nowif (ATE.didValuesChange(this, updateTime)) recreate()} }You can also apply theming to views in a Fragment:
publicclassMyFragmentextendsFragment{@OverridepublicvoidonViewCreated(Viewview, BundlesavedInstanceState){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:
publicclassMyActivityextendsATEActivity{privateToolbarmToolbar; @OverridepublicvoidonCreate(BundlesavedInstanceState){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 } @OverridepublicbooleanonCreateOptionsMenu(Menumenu){getMenuInflater().inflate(R.menu.main, menu); returnsuper.onCreateOptionsMenu(menu)} @OverridepublicbooleanonMenuOpened(intfeatureId, Menumenu){// When the overflow menu opens, a tint is applied to the widget views insideATE.applyMenu(mToolbar); returnsuper.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:
publicstaticclassMyAdapterextendsRecyclerView.Adapter<MyAdapter.MyViewHolder>{publicMyAdapter(){} @OverridepublicMyViewHolderonCreateViewHolder(ViewGroupparent, intviewType){Viewlist = LayoutInflater.from(parent.getContext()) .inflate(R.layout.list_item, parent, false); returnnewMyViewHolder(list)} @OverridepublicvoidonBindViewHolder(MyViewHolderholder, intposition){// Setup views } @OverridepublicintgetItemCount(){return20} publicstaticclassMyViewHolderextendsRecyclerView.ViewHolder{publicMyViewHolder(ViewitemView){super(itemView); // It's recommended you only apply the theme the first time the holder is createdATE.apply(itemView.getContext(), itemView)} } }For ListViews:
publicstaticclassMyAdapterextendsBaseAdapter{@OverridepublicintgetCount(){return20} @OverridepublicObjectgetItem(intposition){returnnull} @OverridepubliclonggetItemId(intposition){returnposition} @OverridepublicViewgetView(intposition, ViewconvertView, ViewGroupparent){if (convertView == null){convertView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.list_item, parent, false); // Only apply the first time the view is createdATE.apply(convertView.getContext(), convertView)} returnconvertView} }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 viewThere'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:
intprimaryColor = 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.
publicclassMyActivityextendsAppCompatActivityimplementsATEStatusBarCustomizer{@ColorInt@OverridepublicintgetStatusBarColor(){returnColor.RED; // return whatever you want here } }If you haven't used tags before, they can be applied to views directly from your XML layouts:
<Viewandroid: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:
<Viewandroid: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.