Android 主题动态切换框架:Prism

news/2024/7/5 18:34:38

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

 

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

000-GraphPad_PrismPrism 分为三个独立库:

  • 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' } 

目前已发布的版本是 1.0.1,最新版本的链接是https://bintray.com/stylingandroid/maven/prism/_latestVersion。

添加好必要的依赖就可以使用 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:

002 Scrolling

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

003 Swiping

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

004 Tapping

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

005 Over-Scroll

最后我们来说一下 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:

006 Prism Palette

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

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


http://www.niftyadmin.cn/n/2422840.html

相关文章

openmp 互斥锁 mysql_并发读写OpenMp中的共享变量

我基本上有三个关于OpenMp的问题。并发读写OpenMp中的共享变量Q1。 OpenMp是否提供互斥共享变量?考虑下面的三个嵌套循环的简单矩阵乘法代码,使用C中的OpenMp并行化。这里A,B和C是动态空间分配双**类型的变量。线程数被适当分配一个值。#prag…

java idgenerator_Java IdGenerator.newId方法代码示例

import com.hazelcast.core.IdGenerator; //导入方法依赖的package包/类/*** {inheritDoc}*/Overridepublic void start(ClusterManager manager) throws AndesException{this.manager manager;/*** register topic listeners for cluster events. This has to be done* after…

Gartner:2016年十大信息安全技术(含解读)

在刚刚结束的2016年Gartner安全与风险管理峰会上,发布了2016年十大信息安全技术(http://www.gartner.com/newsroom/id/3347717)。这里提及的10大技术基本上都在以往的报告中详细阐述过。这10大技术分别是:1)云访问安全…

English - every和each的用法和区别

两者都有“每个”的意思,但用法不同: (1)each具有名词和形容词的功能,every只有形容词的功能. (2)each指两个或两个以上的人或事物中的“每个”;every是指三个以上的人或事物的“全体”,和all的意思相近.如…

Codeforces Round #358 (Div. 2) E. Alyona and Triangles 随机化

E. Alyona and Triangles题目连接: http://codeforces.com/contest/682/problem/E Description You are given n points with integer coordinates on the plane. Points are given in a way such that there is no triangle, formed by any three of these n point…

Python文件处理之文件写入方式与写缓存(三)

Python的open的写入方式有: write(str):将str写入文件 writelines(sequence of strings):写多行到文件,参数为可迭代对象 首先来看下writelines()这个方法: 1 f open(blogCblog.txt, w) #首先先创建一个文件对象,打开方式为w 2 …

python旋转背景图_在python中旋转图像并移除背景

使用cv2.boundingRect将提供适合轮廓的最小非旋转矩形。cv2.boundingRect结果:您将需要使用cv2.boundingRect,而不是使用cv2.minareRect来获得一个适合轮廓的矩形。cv2.minarerect结果:在获得旋转矩形信息后,需要找到模型点与当前…

java 垃圾回收_java 垃圾回收总结(1)

以前看过很多次关于垃圾回收相关的文章,都只是看过就忘记了,没有好好的整理一下,发现写文章可以强化自己的记忆。java与C,c有很大的不同就是java语言开发者不需要关注内存信息,不会显式的直接操作内存,而是通过jvm虚拟…