Prism(棱镜) 是一个全新的 Android 动态主题切换框架,虽然是头一次发布,但它所具备的基础功能已经足够强大了!本文介绍了 Prism 的各种用法,希望对你会有所帮助,你也可以对它进行扩展,来满足开发需求。



先说一下 Prism 的诞生背景。其实我没打算一上来就写个框架出来,当时在给 Styling Android 博客写一些使用 ViewPager 来实现 UI 动态着色的系列文章,文中用到的代码被我重构成适合讲解用的组件,然后我发现这些代码可以整理成一个简洁的 API,于是乎便有了做 Prism 框架的想法。我把 Prism 拿给我比较认可的几个人看,他们都觉得不错,这样我就一点点把它做成了库。经过反复使用,我觉得这个 API 在保持架构简洁的同时已经具备了很多的功能,就决定把它发布出来了跟大家分享。

Prism 分为三个独立库:


  • prism 是 Prism 的核心库
  • prism-viewpager 实现了 ViewPager 与核心库的对接
  • prism-palette 实现了 Palette 调色板与核心库的对接

将它们拆分开的原因是核心库 prism 没有外部依赖,身量轻巧,很容易添加到项目中去,而 prism-viewpager 和 prism-palette 要依赖于外部相关的支持库。如果项目不需要这两个扩展库,就没有其他依赖了;假如应用程序用到了 ViewPager,那该项目就包含了 ViewPager 所依赖的支持库,这时再引入 prism-viewpager 库,其所带来的系统开销大可忽略不计。

Prism 已发布到 jCenter 和 Maven Central 上,如果你的项目已使用了其中一个做为依赖仓库,那只要在 build.gradle 的 dependencies 选项下添加 Prism 库就好。以下是添加了 prism 和 prism-viewpager 两个库的代码(最后两行):

apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion "23.0.0 rc3"
defaultConfig {
applicationId "com.stylingandroid.prism.sample.palette"
minSdkVersion 7
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:design:22.2.0'
compile 'com.android.support:support-v4:22.2.0'
compile 'com.stylingandroid.prism:prism:1.0.1'
compile 'com.stylingandroid.prism:prism-viewpager:1.0.1'
}


添加好必要的依赖就可以使用 Prism 了。

Prism 基本上由三种对象类型构成:SetterFilter 和 Trigger

Setter 用来设置 UI 对象的颜色,一般是 View 但也可以是其他元素,后面会讲到。它的基本用法是将​​setColour(int colour)​​​(或 ​​setColor(int color)​​​)映射到 View 封装的某个方法上。例如,内置的 ViewBackgroundSetter 会映射到 ​​setBackgroundCOLOR(int color)​​ 上。有时 Setter 在不同版本的 Android 上会产生不同的效果,例如 StatusBarSetter 在 Android Lollipop (5.0) 之前的系统上不起作用,因为 Lollipop 之前的版本不支持改变 StatusBar 的颜色。不过 Prism 会随机应变,不会引起程序崩溃,请放心使用,一切交由 Setter 搞定。

Prism 内置有如下几个基本的 Setter:


​FabSetter(FloatingActionButton fab)​​为 Android Design Support Library 中的 FloatingActionButton(简写 FAB)设置背景色。​​StatusBarSetter(Window window)​​设置指定窗体的状态栏颜色,注意它的操作对象并不是 View。​​TextSetter(TextView textView)​​设置 TextView 中的文本颜色。​​ViewBackgroundSetter(View view)​​设置 View 的背景颜色。

当然,你也可以创建新的 Setter 给自定义 View 中的不同组件设置颜色,或者给同一个 View 创建多个 Setter 来设置不同的属性,同时对不同组件进行着色。只要把自定义的 Setter 添加到 Prism 中即可生效。

Filter 可以对颜色进行转化处理。一般向 Prism 传入的是一个颜色值,有时我们可能需要把该颜色的不同色度应用到不同的 UI 组件上,这时要用 Filter 将颜色进行一下转换再输出。内置的基本 Filter 有:


  • ​IdentifyFilter()​​返回与输入相同的颜色。
  • ​ShadeFilter(float amount)​​将输入颜色与黑色混合进行加深处理。amount 为 0 到 1 之间的浮点数,代表黑色的混合比率。当 amount 为 0 时,输出颜色就是输入颜色;为 1 时,则输出纯黑色。
  • ​TintFilter(float amount)​​将输入颜色与白色混合进行加亮处理。amount 为 0 到 1 之间的浮点数,代表白色的混合比率。当 amount 为 0 时,输出颜色就是输入颜色;为 1 时,则输出纯白色。

Trigger 是颜色变化时所触发的事件。通常它会调用 Prism 实例上的 ​​setColour(int colour)​​,将颜色变化的消息传递给在该实例上注册过的所有 Setter 方法。

因为 Trigger 需要额外的依赖库,所以 Prism 核心库没有将它包含进去,但在 ViewPager 和 Palette 的扩展库中都有提供。

接下来我们要将 Prism 这三个组件整合起来,其实每个 Prism 实例的作用就是如此。每个实例可以有多个 Trigger 或者一个都没有,同样也可以有一个或多个 Setter。每个 Setter 可以绑定一个 Filter,Filter 把 Trigger 发过来的颜色转换后再交还给 Setter。

Prism 还提供了一些智能的工厂方法,它们会为传入的数据自动创建 Setter 方法,比如向​​Prism.Builder.background()​​ 传入 FloatingActionButton,Prism 会自动创建出 FabColourSetter。

每个 Prism 实例会使用 builder 模式来构建和整合组件,然后与 Trigger 绑定,对触发事件做出响应。下面来看一下如何创建一个 Prism 实例:

// MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = (TextView) findViewById(R.id.text_view);
AppBarLayout appBar = (AppBarLayout) findViewById(R.id.app_bar);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
setSupportActionBar(toolbar);
// --- 创建 Prism 实例 ---------------------
Filter tint = new TintFilter(TINT_FACTOR_50_PERCENT);
prism = Prism.Builder.newInstance()
.background(appBar)
.background(getWindow())
.text(textView)
.background(fab, tint)
.build();
// ----------------------------------------
fab.setOnClickListener(this);
setColour(currentColour);
}
@Override
protected void onDestroy() {
if (prism != null) {
prism.destroy();
}
super.onDestroy();
}

上面的代码大部分都是基本的 Android 开发操作,不需要特别的解释。重点看一下创建 Prism 实例的部分——先创建一个将输入颜色加亮 50% 的 Filter(TintFilter),然后创建 Prism.Builder 实例,并添加 AppBar 实例(这会为 AppBar 创建一个 Setter 来设置背景色)、Window(为 StatusBarColour 创建 Setter 来设置状态栏颜色)、TextView(使用 ​​text(TextView)​​​ 来设置文字颜色),以及 FloatingActionButton(设置 FAB 背景色并应用第一步中的 TintFilter)。最后用 ​​build()​​ 来完成 Prism 实例的构建。

现在所有组件都被串联了起来,此时只要调用该实例上的 ​​setColour(int colour)​​ 就可以同时改变这些组件的颜色:

prism.setColour(0xFF0000);

代码最后明确使用了 ​​onDestroy()​​ 来清除 Prism 实例。其实严格来说这一步并不是必须要有,因为等到 Activity 被清除后,系统不会保留对 Prism 实例的引用,垃圾回收器会将 Prism 实例处理掉。不过如果后面真不会再用的话,及时做下手工清理也无妨。

Prism 的基本用法就是这样,只要在 ​​onCreate()​​ 中增加六行代码,就能同时改变各组件的颜色(下面使用了 FloatingActionButton 来触发颜色切换)。


把 Setter 和 Filter 配合起来使用省去了大量的样板代码,让事情简单好多,实际上它们完成的工作并不复杂,但如果搭配 Trigger 使用,情况就不一样了。

首先将 prism-viewpager 做为依赖添加到项目中来,对应的 build.gradle 内容如下:

...
dependencies {
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:design:22.2.0'
compile 'com.stylingandroid.prism:prism:1.0.1'
compile 'com.stylingandroid.prism:prism-viewpager:1.0.1'
}

Trigger 是 Prism 实例最前方的关卡,它来触发主题颜色的改变。我们先来看一下 ViewPagerTrigger 如何根据用户操作来触发 ViewPager 改变颜色。ViewPager 的 Adaptor 要为每个页面位置提供颜色信息,这需要通过 ColourProvider 接口来完成(或 ColorProvider,如果不介意使用这种拼写方式所带来的少许性能损失的话 ​​1​​):

// ColourProvider.java
public interface ColourProvider {
@ColorInt int getColour(int position);
int getCount();
}
// ColorProvider.java
public interface ColorProvider {
@ColorInt int getColor(int position);
int getCount();
}

如果你用过 PagerTitleStrip 或 Design Library 中的 TabLayout,那对给每个页面位置提供一个标题的做法就不陌生了。ColourProvider 接口就是这个作用,只不过它把标题的字符串换成了 RGB 颜色值。Adapter 已内置了 ​​getCount()​​ 方法,所以在继承 Adapter 时不用重新定义这个方法,可以按下面的示例来实现自己的 Adaptor:

// RainbowPagerAdapter.java
public class RainbowPagerAdapter extends FragmentPagerAdapter implements ColourProvider {
private static final Rainbow[] COLOURS = {
Rainbow.Red, Rainbow.Orange, Rainbow.Yellow, Rainbow.Green,
Rainbow.Blue, Rainbow.Indigo, Rainbow.Violet
};
private final Context context;
public RainbowPagerAdapter(Context context, FragmentManager fragmentManager) {
super(fragmentManager);
this.context = context;
}
@Override
public Fragment getItem(int position) {
Rainbow colour = COLOURS[position];
return ColourFragment.newInstance(context, getPageTitle(position), colour.getColour());
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
FragmentManager manager = ((Fragment) object).getFragmentManager();
FragmentTransaction trans = manager.beginTransaction();
trans.remove((Fragment) object);
trans.commit();
super.destroyItem(container, position, object);
}
@Override
public int getCount() {
return COLOURS.length;
}
@Override
public CharSequence getPageTitle(int position) {
return COLOURS[position].name();
}
@Override
public int getColour(int position) {
return COLOURS[position].getColour();
}
private enum Rainbow {
Red(Color.rgb(0xFF, 0x00, 0x00)),
Orange(Color.rgb(0xFF, 0x7F, 0x00)),
Yellow(Color.rgb(0xCF, 0xCF, 0x00)),
Green(Color.rgb(0x00, 0xAF, 0x00)),
Blue(Color.rgb(0x00, 0x00, 0xFF)),
Indigo(Color.rgb(0x4B, 0x00, 0x82)),
Violet(Color.rgb(0x7F, 0x00, 0xFF));
private final int colour;
Rainbow(int colour) {
this.colour = colour;
}
public int getColour() {
return colour;
}
}
}

我们得到了一个实现了 ColourProvider 接口的 Adaptor,现在可以把它跟 ViewPagerTrigger 一起使用了:

// MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final float TINT_FACTOR_50_PERCENT = 0.5f;
private DrawerLayout drawerLayout;
private View navHeader;
private AppBarLayout appBar;
private Toolbar toolbar;
private TabLayout tabLayout;
private ViewPager viewPager;
private FloatingActionButton fab;
private Prism prism = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
navHeader = findViewById(R.id.nav_header);
appBar = (AppBarLayout) findViewById(R.id.app_bar);
toolbar = (Toolbar) findViewById(R.id.toolbar);
tabLayout = (TabLayout) findViewById(R.id.tab_layout);
viewPager = (ViewPager) findViewById(R.id.viewpager);
fab = (FloatingActionButton) findViewById(R.id.fab);
setupToolbar();
setupViewPager();
}
@Override
protected void onDestroy() {
if (prism != null) {
prism.destroy();
}
super.onDestroy();
}
private void setupToolbar() {
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setHomeAsUpIndicator(R.drawable.ic_menu);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.app_title);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
drawerLayout.openDrawer(GravityCompat.START);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void setupViewPager() {
RainbowPagerAdapter adapter = new RainbowPagerAdapter(this, getSupportFragmentManager());
viewPager.setAdapter(adapter);
Filter tint = new TintFilter(TINT_FACTOR_50_PERCENT);
Trigger trigger = ViewPagerTrigger.newInstance(viewPager, adapter);
prism = Prism.Builder.newInstance()
.add(trigger)
.background(appBar)
.background(getWindow())
.background(navHeader)
.background(fab, tint)
.colour(viewPager, tint)
.build();
tabLayout.setupWithViewPager(viewPager);
viewPager.setCurrentItem(0);
}
}

在 ​​setupViewPager()​​ 中,我们先创建了一个 RainbowPagerAdapter 实例,并把它应用到 ViewPager 上,然后又创建了一个加亮 FAB 背景色的 TintFilter, 以及与 ViewPager 和 Adaptor 相关联的 Trigger。

接着以同样的方式再创建一个 Prism 实例,这次我们为 Prism 绑定了更多的组件,并添加了刚才做好的 Trigger。你可能注意到 ViewPager 实例被设置了颜色,这会改变 ViewPager 滑动到边界时产生的发光效果的颜色(因为不同版本的系统会用不同的方式来处理发光效果,但 Prism 内部会处理好这些差异)。

然后把 TabLayout 和 ViewPager 进行绑定(TabLayout 要求这样做,但 Prism 并不需要这样),最后把 ViewPager 的初始页面设为第一页。好了大功告成,现在主题色会随着标签页的切换而改变,请看 Demo:

细心的人可能会发现其间的颜色过渡看起来并不生硬,颜色是随着用户的拖拽而逐渐产生变化:


还有一些更微妙的细节。如果用户选择了间隔很远的标签页面,正常情况会过渡显示从开始到结束标签之间的每种颜色,从视觉上说会略显唐突和不自然,而 ViewPagerTrigger 只选择开始和结束标签的两种颜色来做平滑过渡(也就是黄色 YELLOW 和紫色 VIOLET,跳过 GREEN、BLUE 和 INDIGO):


这是 ViewPager 滑动到边界时的动画效果:


最后我们来说一下 prism-palette 的用法。先将它做为依赖添加到项目中来,对应的 build.gradle 内容如下:

...
dependencies {
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:design:22.2.0'
compile 'com.stylingandroid.prism:prism:1.0.0'
compile 'com.stylingandroid.prism:prism-palette:1.0.0'
}

PaletteTrigger 使用起来非常简单,只要创建一个 PaletteTrigger 实例,再把它添加到 Prism.Builder 上:

paletteTrigger = new PaletteTrigger();
prism = Prism.Builder.newInstance()
.add(paletteTrigger)
.
.
.
.build();

接下来,我们可以通过调用 PaletteTrigger 的 ​​setBitmap(Bitmap bitmap)​​ 方法来触发颜色变化。这会创建一个新的 Palette 实例,等到 Palette 从图像中提取完色样后就去触发 Prism。

要想正确地为相关联的 UI 组件着色,我们需要了解 Palette 的工作原理。

Palette 可以从一张图片中提取出最多 6 种不同的色样:


  • 鲜艳
  • 鲜艳浓
  • 鲜艳淡
  • 柔色
  • 柔色浓
  • 柔色淡

每种色样又可以分离出 3 种色值:


  • 原色
  • 适用于以原色为背景色的标题文本的色值
  • 适用于以原色为背景色的正文的色值

这样从 Palette 中我们可以获取最多 18 种不同的颜色。

PrismTrigger 提供了许多工厂方法,以 Filter 的形式返回不同的色样,通过使用 modifier 让 Filter 决定要不要使用原色、标题颜色和正文颜色。实际上这是利用 Filter 机制为每一个与 Prism 关联起来的 UI 组件找到合适的颜色。

例如要给标题使用「鲜艳浓」的颜色,只要将有效的工厂方法链式连接起来组成所需的 Filter:

Filter darkVibrantTitle = paletteTrigger.getDarkVibrantFilter(paletteTrigger.getTextFilter());

如果不设置 Filter 那么 Palette 会默认使用「鲜艳」的原色色值,但建议按需要设置好 Filter。目前,如果 Palette 没找到指定色样,就会应用透明效果,即把被着色的 UI 组件完全隐藏起来。这种处理方法并不理想,我们会在以后版本中做出改进。

至此 PaletteTrigger 跟 Prism 完全绑定好了:

View vibrant = findViewById(R.id.swatch_vibrant);
View vibrantLight = findViewById(R.id.swatch_vibrant_light);
View vibrantDark = findViewById(R.id.swatch_vibrant_dark);
View muted = findViewById(R.id.swatch_muted);
View mutedLight = findViewById(R.id.swatch_muted_light);
View mutedDark = findViewById(R.id.swatch_muted_dark);
titleText = (TextView) findViewById(R.id.title);
bodyText = (TextView) findViewById(R.id.body);
paletteTrigger = new PaletteTrigger();
prism = Prism.Builder.newInstance()
.add(paletteTrigger)
.background(vibrant, paletteTrigger.getVibrantFilter(paletteTrigger.getColour()))
.background(vibrantLight, paletteTrigger.getLightVibrantFilter(paletteTrigger.getColour()))
.background(vibrantDark, paletteTrigger.getDarkMutedFilter(paletteTrigger.getColour()))
.background(muted, paletteTrigger.getMutedFilter(paletteTrigger.getColour()))
.background(mutedLight, paletteTrigger.getLightMutedFilter(paletteTrigger.getColour()))
.background(mutedDark, paletteTrigger.getDarkMutedFilter(paletteTrigger.getColour()))
.background(titleText, paletteTrigger.getVibrantFilter(paletteTrigger.getColour()))
.text(titleText, paletteTrigger.getVibrantFilter(paletteTrigger.getTitleTextColour()))
.background(bodyText, paletteTrigger.getLightMutedFilter(paletteTrigger.getColour()))
.text(bodyText, paletteTrigger.getLightMutedFilter(paletteTrigger.getBodyTextColour()))
.add(this)
.build();

6 个 View 对象各自采用了上述 6 种色样的一种,2 个 TextView 中标题使用了「鲜艳」,正文了使用「柔色浅」。

你可能还注意到我们把 Activity 注册成一个 Setter,这是为了在 Palette 完成色样提取后收到回调,因为处理较大图像时速度可能会慢。这样只有等色样提取完成后 ImageView 中的图像才会被更新,用户体验会稍稍好一点,图像更新和 UI 颜色刷新同步进行。请看 Demo:


在上面的示例中我们实际并没绑定 UI,只是演示一下怎样提取各种色样以及如何应用。但根据前面讲过的内容,相信加入绑定也不是难事。

这些就是 Prism 的基本用法。如果 Prism 开发还会继续,我们会带来更多的内容。文中的所有例子可以从 ​​​