项目地址:https://github.com/zibuyuqing/RoundCorner
app体验地址:https://www.coolapk.com/apk/180019

需求分析

目前大部分手机的通知都是以顶部弹出框的形式提示用户,这样的设计肯定是符合交互习惯的,但是对于爱玩jiji的我,这种逻辑还不够刺激,甚至有些死板,那就要思考了——用什么方式能即不影响用户操作又能告诉用户来通知了呢?想起之前看过一款曲面屏手机来电时的效果(屏幕朝下时我们依然可以通过那个曲面看到炫酷的光线),来了灵感,姑且就撸一套通知系统吧,也为手机加点灵气。

效果展示

当我们手机来通知时,屏幕边缘会产生炫酷的光效,目前实现了五种动画,分别是:转圈圈、一飞冲天、淡入淡出、中出(不要在意名字,根据动画特点取得,噗...)和闪点点,动画的效果均可自定义,我截取了一部份无码动图,请勿传播:


转圈圈.gif
一飞冲天.gif
淡入淡出.gif
中出.gif

所用技术

想要实现此项目,您需要了解一下技术点,我在这里简单描述一下

1.通知监听

通知的监听有两种方式可以实现:一种是通过无障碍AccessibilityService监听通知栏(常见的抢红包助手就是这货搞的),来通知时我们就启动动画,但是这种方式不方便对通知识别,因此做应用的绑定有些困难;另外一种是通过NotificationListenerService实现(桌面上的应用通知角标是这货搞的),里面有个onNotificationPosted方法,可以很轻松的知道发通知的应用是哪个。很明显,后者比较适合这个项目。

2.悬浮窗

想要在任何界面都可以出现,那只有悬浮窗咯,当然这里的“任何”只针对android7.0以下的版本,8.0的系统做了很多限制,悬浮窗在某些界面出不来的。

3.自定义view

不难不难,搞定path就行。

4.保活

保活的方式很多,比如前台进程、双进程、JobService,广播监听等,android 5.0以前,通过一定的手段,可以保证应用不被杀死(也就是流氓软件),但是在6.0以后的android系统中,你想要完全保活是不可能了,除非你和某个手机大佬比较熟,把你的应用加入白名单,否则就别想了。项目中使用双进程保活,配合自启动,效果还不错(心态稳)。

view实现

1.构造Path

Path是啥?做Android开发的同学应该都知道path的强大吧,良好的path动画就像刚出浴的美人,让人有一种“莫名”的激动甚至还有点心痒痒(这是比喻)。我们这个功能的所有动画都是path实现的,那就会会她吧。

1.1直角矩形框

很多人想到了Path的addRect方法对吧。但是!这个绘制出来的矩形起点是屏幕的坐上角(0,0),而我们要实现的动画是左右两边对称的,这就不好控制了,所以还是老老实实的lineTo,moveTo吧

            // edge 是线宽的一半,避免所画的线被屏幕边沿遮住一半
            float edge = mStrokeWidth / 2;  // mStrokeWidth为线宽
            mPath.moveTo(mScreenWidth / 2 - edge, edge);
            mPath.lineTo(edge, edge);
            mPath.lineTo(edge, mScreenHeight - edge);
            mPath.lineTo(mScreenWidth - edge, mScreenHeight - edge);
            mPath.lineTo(mScreenWidth - edge, edge);
            mPath.close();

1.2圆角矩形框

Path里也有直接绘制圆角矩形的方法:addRoundRect(..),但是正如之前所说,我们动画是对称的,所以最好从屏幕宽度的一半开始构造

        // 因为加了屏幕圆角功能,所以要判断圆角是否已经显示了
        // mCornerSize 为圆角的半径,依此确定矩形边框圆角大小
        if (isCornersShown()) {
            //起点移到顶部中间
            mPath.moveTo(mScreenWidth / 2 - edge, edge);
            //绘制到左边沿
            mPath.lineTo(mCornerSize, edge);
            //左上角圆弧
            mPath.arcTo(new RectF(edge, edge, (mCornerSize * 2.0f + edge),
                    (mCornerSize * 2.0f + edge)), 270, -90.0f, false);
            
            //绘制左边沿
            mPath.lineTo(edge, mScreenHeight - mCornerSize - edge);
            //左下圆弧
            mPath.arcTo(new RectF(edge, (mScreenHeight - 2 * mCornerSize - edge),
                    (mCornerSize * 2.0f + edge), (mScreenHeight - edge)), 180.0f, -90.0f, false);
            
            mPath.lineTo(mScreenWidth - mCornerSize - edge, mScreenHeight - edge);
            //右下圆弧
            mPath.arcTo(new RectF((mScreenWidth - 2 * mCornerSize - edge), (mScreenHeight - 2 * mCornerSize - edge),
                    (mScreenWidth - edge), (mScreenHeight - edge)), 90.0f, -90.0f, false);
            mPath.lineTo(mScreenWidth - edge, mCornerSize + edge);
            //右上圆弧
            mPath.arcTo(new RectF((mScreenWidth - 2 * mCornerSize - edge), edge,
                    (mScreenWidth - edge), (mCornerSize * 2.0f + edge)), 0.0f, -90.0f, false);
            //回到起点
            mPath.lineTo(mScreenWidth / 2 - edge, edge);
            //闭合
            mPath.close();
        }

这里注意lineTo,MoveTo,addArc和arcTo之间的区别,前人讲的很好,轻点一下:http://android.jobbole.com/83384/

到这我们把最最重要的path构建好了,欣赏一下:


QQ截图20180404134753.png

2.实现动画

2.1准备工作

(1)构思:
动画形式的灵感来源于街边的广告牌,在屏幕四边绘制彩色线条,配合明暗变化达到闪烁效果,线条的长短是截取刚刚绘制的path的一部分,随时间改变截取的长度和位置。本案例实现了五种动画效果,具体长什么样可参照预览图或体验apk(不大不大,1M多)
(2)准备材料
这里准备材料是要准备我们的画笔,颜色渲染器等。画笔style要设置成STROKE,由于我们还要使用混合色,那就要设置Shader了,用哪个呢?想来想去还是LinearGradient比较适合

        mColorShader = new LinearGradient(0, 0, mScreenWidth, mScreenHeight, mMixedColorArr, null, Shader.TileMode.MIRROR);
        mMixedPaint.setShader(mColorShader);

2.2动画实现

我个人比较偏爱属性动画,先来定义一个ValueAnimator,然后我们设置好动画进度监听接口,这也是整个动画系统的驱动者

        //动画状态监听
        mAnimatorListener = new AnimatorListenerAdapter() {
            @Override
            public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
                hide(true);
                mCurrentRepeatCount = 0;// 记录动画重复次数
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                super.onAnimationRepeat(animation);
                mCurrentRepeatCount++;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                hide(false);
                mCurrentRepeatCount = 0;
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
            }
        };
        //动画进度监听
        mValueUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mProgress = (float) animation.getAnimatedValue();
                if (mStateListener != null) {
                    mStateListener.onAnimationRunning(mProgress);
                }
                flush(); // 刷新view属性
            }
        };
        mValueAnimator = ValueAnimator.ofFloat(0, 1);
        mValueAnimator.addUpdateListener(mValueUpdateListener);
        mValueAnimator.addListener(mAnimatorListener);

代码中出现了hide(boolean immediately)和flush()两个方法,其中hide方法是动画结束或者在某些突发情况下让WindowManager把这个view移除掉,参数标记是否立即移除,如果为true我们直接调用removeView,如果为false,我们在执行一个渐隐的动画,不至于那么突兀。flush()炒鸡简单,就是更新view的

    private void flush() {
        if (needChangeAlpha) {
            mMixedPaint.setAlpha((int) (mProgress * 255));
            mPrimaryPaint.setAlpha((int) (mProgress * 255));// 绘制单个色调的画笔
        }
        invalidate();
    }

万物基于progress,这个代表动画的进度,对view和path的处理依据这货,它的值是:(float) animation.getAnimatedValue(),由于我们的过程是0到1,所有计算起来贼方便。
ok,前期准备工作完成,我们来讲重点吧。
(1)淡入淡出效果
这个动画是最简单的,我们做个开胃菜,但是光有渐隐渐显的效果还不够,我们还需要让颜色“流动”起来,视觉上看上去更绚丽,来,上Matrix

    private void drawFadeInOutStyle(Canvas canvas) {
        mTranslationX = mProgress * mScreenWidth;
        mTranslationY = mProgress * mScreenHeight;
        mGradientMatrix.setTranslate(mTranslationX, mTranslationY);
        mColorShader.setLocalMatrix(mGradientMatrix);
        canvas.drawPath(mPath, mMixedPaint);
    }

简单吧,效果参照预览图或体验apk。
(2)闪点点效果
大致知道path怎么用了吧,我们来增加点难度,做一下大部分广告灯的效果——闪点点。这里们会用到DashPathEffect,因为我们的Path是整个闭环,要做出“点”的效果,就要用虚线了。看一下这货的构造函数

public DashPathEffect( float[] intervals,float phase)

第一个参数是一个float数组,长度必须是偶数且>=2,指定了多少长度的实线之后再画多少长度的空白。例如我的是这样的 mPathIntervals = new float[]{mStrokeWidth, mScreenHeight / 80},翻译过来就是每隔 mScreenHeight / 80画一条长为mStrokeWidth的线;第二个参数指定了绘制的虚线相对了起始地址(Path起点)。我们来看一下代码

    private void drawLatticeStyle(Canvas canvas) {
        // step 是混合色数组的index 通过变化我们实现不同颜色切换
        int step = mCurrentRepeatCount % 3; // mCurrentRepeatCount 当前动画重复次数
        mPhase = (int) (step * mPathIntervals[1] / 3);
        mPrimaryPaint.setColor(mMixedColorArr[step]);
        // 渐隐效果
        mPrimaryPaint.setAlpha((int) (mProgress * 255));
        mPathEffect = new DashPathEffect(mPathIntervals, mPhase);
        mPrimaryPaint.setPathEffect(mPathEffect);
        canvas.drawPath(mPath, mPrimaryPaint);
    }

这样我们就实现了闪点效果,之前没贴图,这里我们看一下


闪点点.gif

当然,欢迎体验apk
(3)中出
余下来的三种动画都是在特定的progress截取Path的不同部位,通过控制线条的长短和位置,然后加上显隐变化来实现,这里我们挑最复杂的中出来讲吧。


Screenshot_2018-04-11-15-07-32-198_com.miui.home.png

原谅我画的图比较乱,大家看了这个图可能会很懵逼,没事,我可能解释的更懵逼。
先看一下图中的单词意思
startP : 整条Path的绘制起点
LSP :left start position 左边线条的起始点
LEP :left end position 左边线条终止点
RSP : right start position 右边线条起始点
REP : right end position 右边线条终止点
distance:线条的长度
既然这里有了左右两条线段,我们就要分两边来分别计算起始点,并且根据progress的不同动态改变起始点,达到移动的效果。
因为这里是顺时针转的,为了方便,我将参考点设置为各边的终点。这里要注意“点”的获取,以起始点为例,我们知道了起始点的progress(总progress为1),
那起始点 startPosition = pathLength * startProgress,而pathLength的获取方法如下

PathMeasure measure = new PathMeasure();
measure.setPath(path,false);
pathLength = measure.getLength();

ok,下面看代码

private void drawMiddleOutStyle(Canvas canvas) {
        Path leftMiddlePath = new Path();
        // 整个一圈的 progress 为 1
        // 初始化进度 屏幕左边缘中点 即 1/4
        float leftReferencePro = 0.25f;

        Path rightMiddlePath = new Path();
        // 初始化进度 屏幕右边缘中点 即 3/4
        float rightReferencePro = 0.75f;
        // 线宽设置为屏幕高度的 1/4 ,可随意
        float distance = mScreenHeight >> 2;

        // range的设置是为了在动画刚开始时做一个线段有短变长的效果

        float offsetProcess = distance / mPathLength;
        float range = offsetProcess;


        if (mProgress < range) {
            // 当总进度小于range时 即线段长小于distance时 我们让线段不断变长

            // 改变左边起始点
            float leftStartPro = leftReferencePro - mProgress;
            float leftEndPro = leftReferencePro;
            // 改变右边起始点
            float rightStartPro = rightReferencePro - mProgress;
            float rightEndPro = rightReferencePro;
            //截取path并绘制
            mPathMeasure.getSegment(leftStartPro * mPathLength, leftEndPro * mPathLength, leftMiddlePath, true);
            canvas.drawPath(leftMiddlePath, mMixedPaint);
            mPathMeasure.getSegment(rightStartPro * mPathLength, rightEndPro * mPathLength, rightMiddlePath, true);
            canvas.drawPath(rightMiddlePath, mMixedPaint);

        } else {

            // 当总进度>=range时 即线段长度达到distance时 我们固定线长

            //左边移动线段的起始progress
            float leftCursorStartPro = leftReferencePro - offsetProcess;

            //右边移动的线段
            Path rightCursorPath = new Path();
            float rightCursorStartPro = rightReferencePro - offsetProcess;

            // 右边移动线段起始点
            float rightPosition = rightCursorStartPro * mPathLength;

            if (leftCursorStartPro >= -offsetProcess) {
           
                Path leftCursorPath = new Path();
                float leftPosition = leftCursorStartPro * mPathLength;
                mPathMeasure.getSegment(leftPosition, leftPosition + distance, leftCursorPath, true);
                canvas.drawPath(leftCursorPath, mMixedPaint);
            }
            if (leftCursorStartPro < 0) {
                // 当左边到达起始点时,线段会逐渐缩短到0,这个时候我们需要补上一条新的path,否则就断片了
                Path replenishPath = new Path();
                float replenishPro = 1.0f - Math.abs(leftCursorStartPro);
                float repStartPosition;
                float repEndPosition;
                // 为什么要0.5呢,因为走到一半要缩小
                if (replenishPro > 0.5) {
                    repStartPosition = replenishPro * mPathLength;
                    repEndPosition = repStartPosition + distance;
                } else {
                    repStartPosition = 0.5f * mPathLength;
                    repEndPosition = repStartPosition + mPathLength * (offsetProcess + rightCursorStartPro);
                }
                // 补充线段绘制
                mPathMeasure.getSegment(repStartPosition, repEndPosition, replenishPath, true);
                canvas.drawPath(replenishPath, mMixedPaint);
            }
            // 因为右边的游动线段起始是从0.75开始的,可以一直减到0
            mPathMeasure.getSegment(rightPosition, rightPosition + distance, rightCursorPath, true);
            canvas.drawPath(rightCursorPath, mMixedPaint);

        }

    }

看吧,这一个动画就用了好几条path,比较复杂,好得注释比较详细,如果把这动画搞懂了,另外的动画都是小case了,这里我就不讲了,感兴趣的可以看一下源码哈。

控制实现

通知监听

android中获取通知的方法通常有两种——无障碍和NotificationListenerService,无障碍就像爬虫,首先是不停的监听内容变化,然后对内容解析,提取我们想要的东西,比较麻烦还不稳定,还好系统有个叫NotificationListenerService的服务,这货可以很方便的告诉我们发起通知的是谁以及通知的内容是啥,太适合此功能的实现了。我们来看看怎么用
(1)定义服务
写一个NotificationListener继承自NotificationListenerService,然后重写onNotificationPosted方法,这个方法会传入一个StatusBarNotification的对象,通过这个对象,我们可以获取通知的内容

  @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        super.onNotificationPosted(sbn);

        String who = sbn.getPackageName();//获取发起通知的包名
        Notification notification = sbn.getNotification();//获取通知对象
        Bundle extras = sbn.getNotification().extras;
        String notificationTitle = extras.getString(Notification.EXTRA_TITLE);//通知标题
        CharSequence notificationText = extras.getCharSequence(Notification.EXTRA_TEXT);//获取通知内容
        if(sNotificationsChangedListener != null){
            sNotificationsChangedListener.onNotificationPosted(who);
        }
    }

当然还有很多方法,感兴趣的同学可以研究一下google的文档。
(2)在manifest文件里声明定义好的NotificationListener

        <service android:name="com.zibuyuqing.roundcorner.service.NotificationListener"
            android:label="@string/enhance_notification"
            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService" />
            </intent-filter>
        </service>

(3)申请权限
要成功监听通知,需要得到系统的许可,毕竟这个是比较“危险”的行为,这个权限需要动态申请,当然不同的手机由于系统定制性不同,方案是不一样的,有的需要手动开启,这里我使用一种比较通用的方法

    /**
     * 检查是否有权限
     * @param context
     * @return
     */
    public static boolean checkNotificationListenPermission(Context context) {
        // 获取允许监听通知的包
        Set<String> packageNames = NotificationManagerCompat.getEnabledListenerPackages(context);
        if (packageNames.contains(context.getPackageName())) {
            return true;
        }
        return false;
    }


    /**
     * 申请权限
     */
    private void requestNotificationListenPermission() {
        try {
            Intent intent = new Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS);
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

控制通知线的显隐

优化&适配

是否有导航栏

横竖屏切换

保活&自启动

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,409评论 25 707
  • 参考文章markdown-入门指南数学公式 图片 链接 百度 引用 这是鲁迅散文集 杂草 粗体和斜体 冯晓静 表格...
    青辰m阅读 216评论 0 0
  • 有的鱼是永远都关不住的,因为它们属于天空。 ...
    张来福阅读 248评论 0 0
  • 喜欢你是我做过最不理智的事 每天想你却是我必做的事 也许我未来还会做更离谱的事 放开一切去追你 以前想象过喜欢的人...
    爱上七号少年阅读 492评论 0 0