zoukankan      html  css  js  c++  java
  • Android 主题动态切换框架:Prism

    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 中找到。

  • 相关阅读:
    前端学习之路,前端开发人员如何在团队中规范git commit提交记录
    基于AntV图表库的Ant DeSign Charts图表展示的简单应用
    基于React-Amap组件库的高德地图简单应用
    高效的Coding,前端开发人员值得一看的前端开发工具
    解决git pull拉取更新代码失败,unable to resolve reference ‘refs/remotes/origin/xxx分支名’: reference broken问题
    position: sticky实现导航栏下滑吸顶效果
    javaScript保留三位有效数字
    封装属于自己的axios请求
    微博立场检测 60分Baseline
    Sequence to Sequence Learning with Neural Networks论文阅读
  • 原文地址:https://www.cnblogs.com/android-blogs/p/5586352.html
Copyright © 2011-2022 走看看