【开源项目解析】背景有波浪效果的TextView——从Titanic项目学习BitmapShader的使用

Hello,好久没写文章了,有木有想我呀~

正式工作已经过去一个月了,发现在青岛实习和在北京工作,感觉完全不一样呢~

现在每天晚上回到住的地方,都累的想睡觉…所以也没心情写太多文章和大家分享了,不过我会尽快调整状态,重振雄风的!(哪里起来怪怪的…)

      • 项目介绍
      • 我的想法
      • 实现思路

项目介绍

这篇文章,会介绍一个开源项目,叫做Titanic,是的,中文名就叫“泰坦尼克”…

下面是项目地址

https://github.com/RomainPiel/Titanic

要实现的效果是下面这样滴

我的想法

如果你是一个程序员,那么在你第一眼看到这个效果的时候,你可能会想,如果我要实现类似的效果,应该怎么做呢?

我不知道你们会有什么样的思路,我第一眼看到的时候,我想起了PorterDuffXfermode,前面是一张文字的图片,后面一张波浪的图片,然后设置相交模式,从而出现这种文字和图片的交叉效果,不断的改变后面图片位置,实现动态的效果。

当然,我只是想了一想,然后就抱着这个想法看源码去了,看完源码才发现,作者的实现思路更加的NB~通过BitmapShader实现了这种效果,下面,我会介绍着源码,给你介绍一下实现思路,非常简单,不要眨眼哦~

实现思路

Titanic这个项目非常的简单,只有两个类,分别是Titanic和TitanicTextView,那么如何使用呢?非常简单:

TitanicTextView tv = (TitanicTextView) findViewById(R.id.my_text_view);
        tv.setTypeface(Typefaces.get(this, "Satisfy-Regular.ttf"));
        new Titanic().start(tv);

如果你不想设置特殊字体,那么中间的代码删除也可以,两行代码搞定,是不是非常easy~

那么Titanic这个类是干嘛的呢?

为了方便理解和观看,我将不重要代码省略了,你可以对照源码观看

我们调用了Titanic的start()之后,就执行了下面的代码了:

public void start(final TitanicTextView textView) {

        final Runnable animate = new Runnable() {
            @Override
            public void run() {

                textView.setSinking(true);

                // horizontal animation. 200 = wave.png width
                ObjectAnimator maskXAnimator = ObjectAnimator.ofFloat(textView, "maskX", 0, 200);
                maskXAnimator.setRepeatCount(ValueAnimator.INFINITE);
                maskXAnimator.setDuration(1000);
                maskXAnimator.setStartDelay(0);

                int h = textView.getHeight();

                // vertical animation
                // maskY = 0 -> wave vertically centered
                // repeat mode REVERSE to go back and forth
                ObjectAnimator maskYAnimator = ObjectAnimator.ofFloat(textView, "maskY", h/2, - h/2);
                maskYAnimator.setRepeatCount(ValueAnimator.INFINITE);
                maskYAnimator.setRepeatMode(ValueAnimator.REVERSE);
                maskYAnimator.setDuration(10000);
                maskYAnimator.setStartDelay(0);

                // now play both animations together
                animatorSet = new AnimatorSet();
                animatorSet.playTogether(maskXAnimator, maskYAnimator);
                animatorSet.setInterpolator(new LinearInterpolator());
                animatorSet.addListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        textView.setSinking(false);

                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                            textView.postInvalidate();
                        } else {
                            textView.postInvalidateOnAnimation();
                        }

                        animatorSet = null;
                    }
                    省略...
                });

                if (animatorListener != null) {
                    animatorSet.addListener(animatorListener);
                }

                animatorSet.start();
            }

        if (!textView.isSetUp()) {
            textView.setAnimationSetupCallback(new TitanicTextView.AnimationSetupCallback() {
                @Override
                public void onSetupAnimation(final TitanicTextView target) {
                    animate.run();
                }
            });
        } else {
            animate.run();
        }
        };

这段代码是不是非常简单?但是有点奇怪啊,里面为什么声明Runnable对象呢?而且这里没开线程啊,而是直接调用的run(),好诡异的代码!

其实这个不需要多想,作者只不过是为了代码复用,所用把需要的代码放在run()方法,方便调用,但是这个代码风格我并不喜欢,因为这会让其他阅读代码的人产生误会,比如我就为了这一块代码,想了半个多小时他为什么这样写…

我们看不惯,就可以改成下面的这种方式:

public void start(final TitanicTextView textView) {

        if (!textView.isSetUp()) {
            textView.setAnimationSetupCallback(new TitanicTextView.AnimationSetupCallback() {
                @Override
                public void onSetupAnimation(final TitanicTextView target) {
                    run(textView);
                }
            });
        } else {
            run(textView);
        }
    }

    private void run(final TitanicTextView textView) {
        textView.setSinking(true);

        ObjectAnimator maskXAnimator = ObjectAnimator.ofFloat(textView, "maskX", 0, 200);
        maskXAnimator.setRepeatCount(ValueAnimator.INFINITE);
        maskXAnimator.setDuration(1000);
        maskXAnimator.setStartDelay(0);

        int h = textView.getHeight();

        ObjectAnimator maskYAnimator = ObjectAnimator.ofFloat(textView, "maskY", h / 2, -h / 2);
        maskYAnimator.setRepeatCount(ValueAnimator.INFINITE);
        maskYAnimator.setRepeatMode(ValueAnimator.REVERSE);
        maskYAnimator.setDuration(10000);
        maskYAnimator.setStartDelay(0);

        // now play both animations together
        animatorSet = new AnimatorSet();
        animatorSet.playTogether(maskXAnimator, maskYAnimator);
        animatorSet.setInterpolator(new LinearInterpolator());
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                textView.setSinking(false);

                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                    textView.postInvalidate();
                } else {
                    textView.postInvalidateOnAnimation();
                }

                animatorSet = null;
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        animatorSet.start();
    }

都是为了代码复用,这种代码风格我更喜欢一些。

ok,不扯淡,我们看看在start()里面到底做了些什么!

textView.setSinking(true);

        ObjectAnimator maskXAnimator = ObjectAnimator.ofFloat(textView, "maskX", 0, 200);
        maskXAnimator.setRepeatCount(ValueAnimator.INFINITE);
        maskXAnimator.setDuration(1000);
        maskXAnimator.setStartDelay(0);

        int h = textView.getHeight();

        ObjectAnimator maskYAnimator = ObjectAnimator.ofFloat(textView, "maskY", h / 2, -h / 2);
        maskYAnimator.setRepeatCount(ValueAnimator.INFINITE);
        maskYAnimator.setRepeatMode(ValueAnimator.REVERSE);
        maskYAnimator.setDuration(10000);
        maskYAnimator.setStartDelay(0);

这段代码也很好理解,设置了个标志位,表示我们要开始下沉了哈!然后使用两个ObjectAnimator来进行动画,这个动画是控制干嘛的呢?

看里面的参数,maskX和maskY,作者的注释写的很清楚,maskX的范围是0-200,200是图片的高度,什么图片?当然是后面波浪效果的图片啦,见下图(啥?没有图片!?白色的看不出来…右键保存看吧):

注意哦,这张图片的上半部分是透明的,下面是白色的,宽高为200*300。

maskY的变化范围是h/2到-h/2,h则是TextView的高度,也就是上下浮动高度为h,那么这个maskX和maskY到底是干嘛的,这两个属性控制的是什么呢?

首先,我们知道,如果要使用ObjectAnimator实现这种效果,那么我们的控件里面必须有这个属性的getter和setter才行,所以这两个属性肯定是自定义的,我们去看TitanicTextView的代码验证一下:

 public float getMaskX() {
        return maskX;
    }

    public void setMaskX(float maskX) {
        this.maskX = maskX;
        invalidate();
    }

    public float getMaskY() {
        return maskY;
    }

    public void setMaskY(float maskY) {
        this.maskY = maskY;
        invalidate();
    }

果然,我们看到,作者自己定义了这两个成员变量,那么改变这两个值到底有什么作用呢?我们找一下代码里面有什么地方用到了这两个值:

 @Override
    protected void onDraw(Canvas canvas) {

        if (sinking && shader != null) {
            if (getPaint().getShader() == null) {
                getPaint().setShader(shader);
            }
            shaderMatrix.setTranslate(maskX, maskY + offsetY);
            shader.setLocalMatrix(shaderMatrix);
        } else {
            getPaint().setShader(null);
        }

        super.onDraw(canvas);
    }

耶,在onDraw()里面,在super.onDraw()之前,用到了这两个参数,这里面有几个变量,我们还没有介绍

  • sinking 用于判断是否开始浮动动画
  • shader 这是一个BitmapShader,这是今天我们的男主角
  • shaderMatrix 这是一个变换矩阵,在这里主要进行了setTranslate操作,参数正是我们的maskX和maskY还有offsetY

这个offsetY是啥子?代码里找找

private void createShader() {
         if (wave == null) {
            wave = getResources().getDrawable(R.drawable.wave);
        }

        int waveW = wave.getIntrinsicWidth();
        int waveH = wave.getIntrinsicHeight();

        Bitmap b = Bitmap.createBitmap(waveW, waveH, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);

        c.drawColor(getCurrentTextColor());

        wave.setBounds(0, 0, waveW, waveH);
        wave.draw(c);

        shader = new BitmapShader(b, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
        getPaint().setShader(shader);
        offsetY = (getHeight() - waveH) / 2;
    }

在最后计算了offsetY,TextView的高度减去waveH,然后除以二,这得到的其实就是wave或者说b的y坐标,在这个基础上,再进行以下操作,就可以实现上下左右移动的动画效果

shaderMatrix.setTranslate(maskX, maskY + offsetY);
shader.setLocalMatrix(shaderMatrix);

那么这个shader到底是干嘛的?为啥是我们的男主角?

我们在上面的代码里面,可以看到不仅对offsetY进行了初始化,而且也对shader进行了初始化,里面传入了三个参数

new BitmapShader(b, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);

第一个参数b是设置Shader要使用的Bitmap对象,第二个参数设置x轴上图像的扩展方式,第三个参数则是y轴上的扩展方式,这个参数共有以下三种

  • CLAMP :如果渲染器超出原始边界范围,会复制范围内边缘染色
  • REPEAT :横向和纵向的重复渲染器图片,平铺
  • MIRROR:镜像方式平铺。

那么上面的写法我们就很明白了,作者在x轴上复制重复,在y轴上渲染边缘颜色,那么,wave得到的Shader就是下面这个样子()

画工咋样…^_^

还要注意一点,就是创建BitmapShader时,第一个参数的得来

        int waveW = wave.getIntrinsicWidth();
        int waveH = wave.getIntrinsicHeight();

        Bitmap b = Bitmap.createBitmap(waveW, waveH, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);

        c.drawColor(getCurrentTextColor());

        wave.setBounds(0, 0, waveW, waveH);
        wave.draw(c);

        shader = new BitmapShader(b, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);

首先根据wave的Drawable对象的长宽,构建一个一样大的Bitmap对象,然后去创建了个Canvas对象,然后画了一个和当前文字颜色的背景,这时候,我们Shader上面的透明颜色,就变成了我们的文字颜色,然后给Drawable设置Bounds之后,draw了一下,这样,我们的Bitmap对象,就是一个和wave的Drawable大小相同,下半部分是白色波浪,上半部分是文字颜色背景的一张图片啦,这个时候在创建一个BitmapShader,就可以得到我们想要的了,然后

 getPaint().setShader(shader);

设置给paint,这样用这支笔写出来的字,就是以我们创建好的BitmapShader为背景的字了~

波浪效果呢?

开启动画之后,改变maskX和maskY,然后用矩阵移动BitmapShader的位置,字体的背景颜色就变化咯,而我们看到的效果就是波浪效果~

还记得maskY的变化范围吗?

h/2 到 -h/2

这样就能够从最下面没有波浪,一直增长到白色波浪沾满整个高度,然后就出现这个效果啦~

ok,这篇文章就写到这里,下期我们再见,回家看西游记去了

转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-10 22:09:10

【开源项目解析】背景有波浪效果的TextView——从Titanic项目学习BitmapShader的使用的相关文章

Github项目解析(十一)--&gt;一个简单,强大的自定义广告活动弹窗

转载请标明出处:一片枫叶的专栏 上一篇文章中讲解了我最近写的一个快速集成二维码扫描库,其核心的实现扫描的功能,是通过调用ZXing库实现的.由于在实现二维码扫描功能的时候发现集成二维码扫描功能并不是特别方便,于是有了将其制作成标准库的想法,这个二维码库能够快速,方便的集成二维码扫描功能,项目地址是在:android-zxingLibrary**,在项目开源后有不少同学提出了许多不错的意见,目前也在不断的迭代中,自己也学到了很多. 本文我们将讲解一个简单,强大的广告活动弹窗控件.不少App在打开的

Github项目解析(九)--&gt;实现Activity跳转动画的五种方式

转载请标明出处:一片枫叶的专栏 上一篇文章中我们讲解了在Activity启动过程中获取组件宽高的五种方式.在Activity的启动过程中如果我们直接在生命周期方法中通过view.getWidth()或者是view.getHeight()方法获取组件的宽度和高度其结果都是0,为什么会出现这个问题呢? 其实看过我以前写过的Activity启动流程  Activity布局加载流程  Activity布局绘制流程  的同学应该对Activity的启动流程和其布局加载绘制流程不陌生,Activity的启动

github项目解析(七)--&gt;防止按钮重复点击

转载请标明出处:一片枫叶的专栏 本文中我将介绍一下我自己封装的一个小的工具类库:按钮点击事件类库. 作用: 该类库可以防止按钮重复点击,可以判断网络状态,可以判断用户登录状态,以及自定义验证条件等等. 说明: 其实现的核心原理就是通过自定义实现自身的OnClickListener类,并重写其中的onClick方法,在onClick方法中执行相应的判断逻辑之后回调我们自定义的抽象方法. 具体效果如下图所示: 使用方式 屏蔽多次点击事件 /** * 测试快速点击事件 */ fastButton.se

github项目解析(六)--&gt;自定义实现ButterKnife框架

转载请标明出处:一片枫叶的专栏 目前在  友友用车  项目中使用到了ButterKnife框架,这是一个通过注解的方式简化程序员代码量,自动映射xml布局文件与对象关系的框架.其github上的地址  ButterKnife 这里简单介绍一下他的使用方式:android注解Butterknife的使用及代码分析 (一)使用方式 1)在activity中如何使用 @InjectView(R.id.feedback_content_edit) EditText feedContent; // 意见反

github项目解析(八)--&gt;Activity启动过程中获取组件宽高的三种方式

转载请标明出处:一片枫叶的专栏 上一个github小项目中我们介绍了防止按钮重复点击的小框架,其实现的核心逻辑是重写OnClickListener的onClick方法,添加防止重复点击的逻辑,即为第二次点击与第一次点击的时间间隔添加阙值,若第二次点击的时间间隔与第一次点击的时间间隔小于阙值,则此次点击无效,再次基础上我们又封装了点击组件验证网络Listener,点击组件验证是否登录Listener等,具体可参考:github项目解析(七)–>防止按钮重复点击 本文中我将介绍一下android中A

Android 开源项目android-open-project解析之(二) GridView,ImageView,ProgressBar,TextView

五.GridView StaggeredGridView 同意非对齐行的GridView,类似Pinterest的瀑布流.而且跟ListView一样自带View缓存,继承自ViewGroup 项目地址:https://github.com/maurycyw/StaggeredGridView Demo地址:https://github.com/Trinea/TrineaDownload/blob/master/staggered-gridview-demo.apk?raw=true APP演示样

renren-fast开源项目解析日志—1、项目的部署

renren_fast项目解析日志 一.环境搭建 1.后端部署 (1)下载源码 按照步骤,从码云上down了fast,zip的(引maven项目)项目包. (2)安装lombok插件 安装lombok的jar.相当简单,在eclipse文件夹的跟目录下,使用java -jar lombok.jar 会出现一个红辣椒的界面,按照说明安装就行了!(重点看红框) 目前我的理解lombok(印尼——龙目岛)就是一个用注解替代ide帮我们在javabean中所创建的一些get.set.toString等方

html5 canvas首屏自适应背景动画循环效果代码

模板描述:html5 canvas首屏自适应背景动画循环效果代码 由于动态图太大,怕以后服务器受不了,所以现在都改为静态图了,大家点击演示地址一样的,希望大家喜欢,你们的支持就是小海的动力!! 欢迎大家积极评论,给出宝贵意见 下 载 演示地址 本文地址:html5 canvas首屏自适应背景动画循环效果代码

10个Web前端值得收藏的背景全屏效果展示(附源码)(上)

作为一个前沿的 Web 开发者,对于 HTML5 和 现在流行的3D技术或多或少都有掌握.特别是在移动端大显身手.这篇文章挑选了10个绚丽的背景全景展示效果,希望对你有所帮助. 1.  JS图片背景全屏代码实现物理效果 玩法介绍:可以随意拖动鼠标.按住鼠标左键选中旋转物体.或者按住鼠标滑轮放大或者缩小,有不同的效果,赶紧来体验一下. 源码下载  /  在线演示 2.  CSS3学习 - 网站背景拉伸平铺jQuery插件 这个插件集成了一些非常好的 JavaScript 库,提供一个方便使用的文本