360加速球效果实现

来源:互联网 发布:c语言编程器下载 编辑:程序博客网 时间:2024/05/01 07:02

效果演示:
悬浮小球在移动时会换成一张图片,当松开时,会自动停靠在一侧,并且恢复原来的形式。
点击悬浮小球,会从屏幕底部滑入一个菜单栏。
双击加速球,会有水不断注入的动画效果,并且水面逐渐平静下来。
单击加速球,水面会震荡,最后恢复平静。

360加速球效果

悬浮球

拖动换成一张图

加速球

这个的实现是观看了慕课网视频后,在其基础上做了一些适当的修改,如:在android23以上,如何申请动态权限。因为这个实现需要用到一个危险权限,弹出窗口。有兴趣的可以前往:http://www.imooc.com/learn/693。

整体思路:
可以看下项目结构。在MainActivity开启了一个服务MyFloatService,这个服务获得了FloatViewManager管理类的实例,view包下有悬浮球的view以及加速球的view。而这个管理类则可控制这些view的显示,隐藏,位置。开启服务后,显示了悬浮球,通过对悬浮球进行Touch事件的监听,实现了拖动改变图片,自动停靠在一侧,单击出现菜单栏。在菜单栏的FloatMenuView类中,实现了对自身的Touch监听,双击出现水不断注入效果,单击水面震荡。单击加速球其他的地方则隐藏菜单栏。
项目结构

源码已经开放在github上:https://github.com/My-Zzw/FloatView360Demo

在这篇文章中,简单提一下使用到的技巧,以及一些主要的思路,一些思想,一些需要注意的地方。

1.权限问题
当点击主界面的开启悬浮窗按钮时,开启MyFloatService服务。当服务创建时,进行判断,如果运行在大于等于6.0系统上,则跳转到应用信任界面,允许该应用所有权限。在这个项目中,需要用到 SYSTEM_ALERT_WINDOW 权限,记得要在manifest中注册。

@Override    public void onCreate() {        FloatViewManager manager = FloatViewManager.getInstance(this);        manager.showFloatCircleView();//弹出一个窗口,需要权限。>=23,需要动态申请。        super.onCreate();    }

2.在FloatViewManager中,是如何控制悬浮球以及菜单栏显示和隐藏的。
首先已经实例化了悬浮球View和菜单栏的View

 //实例化悬浮球View floatCircleView = new FloatCircleView(context); //实例化菜单View floatMenuView = new FloatMenuView(context);

已经实例化了View。那么View要显示在界面,就需要一些布局参数,比如view的宽和高,位置在哪里?所以需要一个 WindowManager.LayoutParams 对象。这样,要显示的view已经拿到,如何显示也拿到,那么就可以添加到界面显示了。显示到界面上让用户看到,需要实例化一个 WindowManager 的对象,通过其addView方法添加到窗口。例如显示悬浮窗。

//显示菜单栏    private void showFloatMenuView() {        WindowManager.LayoutParams params2 = new WindowManager.LayoutParams();        params2.width = getScreenWidth();        params2.height = getScreenHeight() - getStatusHeigt();//高度为全屏。        params2.gravity = Gravity.BOTTOM | Gravity.LEFT;        params2.x = 0;        params2.y = 0;        params2.type = WindowManager.LayoutParams.TYPE_PHONE;//布局参数的类型为手机类型。意味着 在所有页面的上面。        params2.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;//不和其他应用抢焦点        params2.format = PixelFormat.RGBA_8888;//设置透明度        windowManager.addView(floatMenuView, params2);//通过manager添加视图到窗口。第一个参数为要添加的view,第二个为view的布局参数    }

3.实现悬浮窗在点击移动时变成一张火箭的图片,松开时自动附着在其中一侧。
首先对悬浮窗注册一个事件监听器。类型为触摸事件。

//注册触摸事件监听器floatCircleView.setOnTouchListener(circleViewOnTouchListener);

接着实现这个事件监听器的触发事件。当用户按下,移动,抬起时。
需要说明的是,当监听到用户在移动这个悬浮球时,调用了悬浮球对象的setDrawState方法通知到这个悬浮球在移动中,那么悬浮球对象就会调用invalidate进行重绘。在onDraw方法去drawBitmap。
当用户松开时,判断最后松开的悬浮窗x坐标如果大于或者小于屏幕的一半,修改悬浮窗的params.x布局参数的x坐标。最后调用updateViewLayout方法,更新悬浮窗。

 //监听的内部类    private View.OnTouchListener circleViewOnTouchListener = new View.OnTouchListener() {        @Override        public boolean onTouch(View view, MotionEvent motionEvent) {            switch (motionEvent.getAction()) {                case MotionEvent.ACTION_DOWN://当按下的时候,获取相对屏幕密度的xy坐标                    startX = motionEvent.getRawX();                    startY = motionEvent.getRawY();                    startX0 = motionEvent.getRawX();                    startY0 = motionEvent.getRawY();                    break;                case MotionEvent.ACTION_MOVE://移动的时候,悬浮球跟着移动                    //获取移动中的坐标                    float x = motionEvent.getRawX();                    float y = motionEvent.getRawY();                    //偏移量                    float dx = x - startX;                    float dy = y - startY;                    //获取布局参数对象。重新设置悬浮球的xy位置                    params.x += dx;                    params.y += dy;                    //移动的过程中,改变样式。通知在移动。                    floatCircleView.setDrawState(true);                    //刷新界面。指定用新的 布局参数params 刷新 floatCircleView在界面的显示                    windowManager.updateViewLayout(floatCircleView,params);                    //起始位置变化为移动后的位置                    startX = x;                    startY = y;                    break;                case MotionEvent.ACTION_UP://当抬起时,悬浮球附着在两旁                    float endX = motionEvent.getRawX();//获取最后的X坐标                    //进行判断.当在屏幕中间线右边或者左边的时,往两边靠拢                    if (endX > getScreenWidth()/2){                        params.x = getScreenWidth() - floatCircleView.width;                    } else {                        params.x = 0;                    }                    //当抬起时,通知移动状态停止。并且悬浮球会重新绘制自身样式                    floatCircleView.setDrawState(false);                    //刷新界面。刷新的是悬浮球floatCircleView在界面的布局参数。而悬浮球样式的改变在其内部。                    windowManager.updateViewLayout(floatCircleView,params);                    //解决可能有的 触摸事件和点击事件 的冲突.                    //如果移动后的X坐标 大于 起始的X坐标6个单位距离,则认为是触摸事件,要终止点击事件的执行                    if (Math.abs(endX - startX0) > 6){//取绝对值                        return true;//返回true的时候,则不会继续往下执行到 onClick 事件。                    } else {                        return  false;                    }                default:                    break;            }            return false;        }    };

4.获取状态栏高度。
这里是采用了反射去获取。这样的好处是能获取到程序运行到具体的设备或者模拟器的状态栏高度。

//获取状态栏的高度    public  int getStatusHeigt (){        //通过反射的方法获取状态栏的高度        try {            Class<?> c = Class.forName("com.android.internal.R$dimen");//反射.class获取类            Object o = c.newInstance();//实例化这个类,得到一个具体的对象            Field field = c.getField("status_bar_height");//获取这个类的field(域)。这个域的对象类型是 这个类里面的一个属性            int x = (Integer)field.get(o);//再从具体对象的一个属性的值            return context.getResources().getDimensionPixelSize(x);//返回。值转换成px        } catch (Exception e) {            e.printStackTrace();            return 0;        }    }

5.隐藏悬浮球或者菜单栏。
例如隐藏菜单栏,拿到窗口管理对象remove即可。

 //隐藏 菜单栏    public void hideFloatMenuView() {        windowManager.removeView(floatMenuView);    }

6.悬浮球 FloatCircleView 的实现。

悬浮球不移动

悬浮球移动状态下,换成一张图片

通过实际效果可以知道,其实现原理很简单。首先是如何绘制。
绘制:绘制有两种状态。一是不移动状态下的绘制,使用两只画笔,一个画笔绘制圆形,一个画笔绘制文本。另一个是移动状态下的绘制。移动状态下是将一个在drawable下的资源图片进行绘制。

//    绘制方法    @Override    protected void onDraw(Canvas canvas) {        //如果移动中,则显示图片,否则正常显示        if (draw){            canvas.drawBitmap(bitmap,0,0,null);        } else {        //绘制圆形        canvas.drawCircle(width/2,height/2,width/2,circlePaint);        //绘制文本        float textWidth = textPaint.measureText(text);//用画笔去测量文本的宽度        float x = width/2 - textWidth/2;//确定文本的x坐标        Paint.FontMetrics metrics = textPaint.getFontMetrics();//获得画笔绘制下的文本规格//      确定文本的y坐标.descent ascent,基准线下的高度,基准线下的高度。为什么不是除2,而是除4?为什么是+而不是-?//      + 是因为文本的初始位置在圆的上方。除4 则可能是因为有默认的行距。以后凡是需要精确的数值,则可以将其都显示出来,再一个个去测试值。        float y = height/2 + (metrics.descent - metrics.ascent)/4;        canvas.drawText(text,x,y ,textPaint);        }    }

如何通知到悬浮球什么时候在移动?通过调用下面这个方法。这个方法会改变状态的标识,并且调用invalidate去重新绘制。

//   在移动中,则进行状态的改变    public void setDrawState(boolean b) {        draw = b;        invalidate();//每当状态改变的时候,需要重新执行draw方法,进行重新绘制    }

要保证移动时候,改变的图片的大小也要和悬浮球大小一致,则需要

//     初始化移动需要的bitmap。并且缩放到合适大小        Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.hj);        bitmap = Bitmap.createScaledBitmap(src,width,height,true);

7.进度球 ProcessView 的实现。

进度球放置在了菜单栏里面。通过实际效果知道,需要三支画笔。绘制圆形,绘制水,绘制文本。
这里写图片描述

7.1 双击进度球动画效果的实现:水不断被注入,并且水面越来越平缓

图解是这样的:
双击效果图解
一支画笔绘制了圆形,另外一支画笔在圆形上绘制了一个“矩形”,不过这个矩形有点特殊的是上面的边是个曲线,其他三边都是直线。
在绘制这个特殊的矩形,是用画笔通过Path路径去绘制,先用lineTo方法依次绘制三条直线的边,在绘制到这个曲线的时候再用rQuadTo方法绘制曲线,也叫贝塞尔曲线。
那么动画效果:水不断被注入,并且水面越来越平缓。是如何实现的呢?
水不断注入,其实就是这个特殊的矩形不断的重新绘制,绘制。并且每次绘制时,矩形的高都是不断增加的,在时间很短的情况下,感觉水面在不断的升高。
水面越来越平缓,则是每次在重新绘制的时候,贝塞尔曲线的振幅不断的在减小。
需要了解贝塞尔曲线可能才能明白在说什么,所以有不明白的可以去百度下Android贝塞尔曲线。

绘制的矩形是在覆盖在圆形上,那么矩形超出圆形的部分如何隐藏?
我们可以在初始化画笔时,设置绘制矩形的画笔的过度模式,也就是图像混合模式。当矩形画笔绘制的图像和其他图像混合时,设置其模式为重叠。也就是说,只显示重叠的部分。

//下面这句代码则是 设置了progressPaint画笔绘制的图像,只显示重叠部分        progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

7.2 单击进度球动画效果的实现:水面不断震荡,最后平缓。

图解:
单击效果图解
其实还是不断的去重新绘制矩形,不过矩形的高度不需要变化,但是贝塞尔曲线需要变化,每绘制一次贝塞尔曲线,那么在下一次则绘制相反的贝塞尔曲线,再下一次则恢复原来的贝塞尔曲线即可。
并且在每次重新绘制的时候,同样需要将振幅减小。

如何每次去计算振幅的减小 以及 如何判断哪次需要将贝塞尔曲线取反。读者可以查看代码结合注释。这里不详细介绍。

    //在这进行绘制.    @Override    protected void onDraw(Canvas canvas) {        //画圆        bitmapCanvas.drawCircle(width/2,height/2,width/2,circlePaint);        //绘制水波纹进度。基本思想是绘制一个矩形。不过矩形除了上面的边使用贝塞尔曲线外,其他都是直线。        path.reset();//重置path的所有属性        //起始点定义。重新规定路径的起始坐标,默认为(0,0)。moveTo()移动画笔。        //这里将起始点的x移动到右边,y则是变化的。也就是矩形的右上角的点        float y = (1 - (float)current_progress/max_progress) * height;        path.moveTo(width,y);        //第二个点定义。右下角,绘制直线。也就是右面的边        path.lineTo(width,height);        //第三个点定义。左下角,绘制直线。也就是下面的边        path.lineTo(0,height);        //第四个点定义。左上角,绘制直线。也就是左面的边        path.lineTo(0,y);        //绘制上面的边。也就是贝塞尔曲线。        //根据进度球宽度,设定需要的贝塞尔曲线的周期。如:250的宽度,那么7个周期为40的即可。        //绘制贝赛尔曲线需要起始点,控制点和结束点。rQuadTo方法只需要两个参数,起始点默认为 未闭合路径的最后一个点。        //一个循环意味着一个周期的贝塞尔曲线        if (!isSingleTab){//双击            //双击效果:贝塞尔曲线逐渐变得平缓,最后成为直线的效果。            //其实就是控制了贝赛尔曲线的振幅。也就是参数的控制点的y坐标。使得y坐标逐渐变成0。也就是rQuadTo中的第二个参数            //在这里设定了振幅为10的话,也就是控制点的y坐标,随着当前进度不断增加到接近目标进度,那么百分比就会不断增加            //这时用1减去他们的百分比,就会不断减小。用这结果去乘振幅。就能使得振幅不断减小.            float d = (1 - ((float)current_progress / progress)) * 10;            for (int i = 0; i < 7 ; i++){                path.rQuadTo(10,d,20,0);                path.rQuadTo(10,-d,20,0);            }        } else {//单击            //单击效果:水不断震荡,最后便于平缓。            //其实就是贝塞尔曲线每次周期不断取反,也就是高的变低,低的变高.            //这里是水波纹波动50次,也就是50个周期.为什么要模2?            //因为count是每次减1。如果模2的结果是0,那么为一个周期。再减1,模2的结果不是0.则为另外一个周期,就可以去实现相反的效果。            //要使得贝赛尔曲线逐渐变得平缓,和上面写的一样道理。使得振幅不断减小.注意count是每次-1            //第一解决贝塞尔曲线不断取反。第二解决贝塞尔曲线振幅也就是控制点的y坐标不断减小。            //以后凡是遇到类似这种,需要多个来配合的,那么先解决一个,再解决下一个。            float d = (float)count/50 * 10;            if (count%2 == 0){                for (int i = 0; i < 7 ; i++){                    path.rQuadTo(20,d,40,0);                    path.rQuadTo(20,-d,40,0);                }            } else {                for (int i = 0; i < 7 ; i++){                    path.rQuadTo(20,-d,40,0);                    path.rQuadTo(20,d,40,0);                }            }        }        path.close();//路径闭合        bitmapCanvas.drawPath(path,progressPaint);        //绘制文本        String text = (int)(((float)current_progress/max_progress) * 100) + "%";        //获取文本宽度。便于规定绘制文本时候的x坐标        float textWidth = textPaint.measureText(text);        //获取文本规格。便于规定绘制文本时候的y坐标;        Paint.FontMetrics metrics = textPaint.getFontMetrics();        float baseLine = height/2 - (metrics.ascent + metrics.descent)/2;        //开始绘制文本        bitmapCanvas.drawText(text, width/2-textWidth/2,baseLine,textPaint);        //利用自定义bitmap的画布绘制完毕后。再通过显示的画布,绘制这自定义的bitmap        canvas.drawBitmap(bitmap,0,0,null);    }

7.3 如何不断去重新绘制?
可以采用Timer定时器去不断重绘。但是这里使用的是 Handle.postDelayed 方法。这里面有个小技巧。也就是postDelayed 方法去执行runable的代码时,如果条件没达到,则再次调用自身,如果条件达到则remove。比如双击动画:
被双击的时,先执行一次

 //开启双击动画        private void startDoubleTapAnimation() {            //利用handler执行一个延迟50毫秒的线程。            //每次线程首先将当前进度++,并且判断。            //如果当前进度没有达到指定进度,先重新绘制,然后再延迟50毫秒后执行同样的线程            //如果达到了,则让当前进度为0,并且关闭。            //本质的效果像开了个定时器,每隔50毫秒去执行,当达到一定条件停止执行            handler.postDelayed(new DoubleTabRunable(),50);        }

接着在执行的方法去做判断,也就是DoubleTabRunable

 //双击时,需要执行的线程。刷新数据,重绘界面。        class DoubleTabRunable implements Runnable {            @Override            public void run() {                current_progress++;                if (current_progress <= progress){                    invalidate();//重新绘制,调用onDraw方法。因为current_progress是变化的,所以重新绘制会使得进度条有变化。                    handler.postDelayed(this,50);//再次调用自己                } else {                    current_progress = 0;                    handler.removeCallbacks(this);                }            }        }

7.4 进度球的绘制,先绘制在了一个自定义的bitmap中,绘制完毕后,再讲这个bitmap绘制在界面中显示。

 //自己画图。创建一个空的bitmap,并且在这bitmap上传入一个画布才能进行绘制。        bitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);        bitmapCanvas = new Canvas(bitmap);

8.菜单栏中放置进度球。
FloatMenuView 继承了LinearLayout。所以这个类本质还是一个View。既然是View的话就会有样式。
之前的悬浮球FloatCircleView 和 进度球 ProcessView 都是通过画笔在OnDraw中去绘制样式。
在FloatMenuView ,则是事先将一个写好的xml格式的布局文件写好,通过View.inflate方法找到这个xml格式的布局文件后,再通过addView方法添加这个布局文件到FloatMenuView ,也就是将这个找到的布局文件和FloatMenuView 绑定。这样在其他地方实例化FloatMenuView 后,显示在窗口的话,样子就是xml布局文件的样式。

 View root = View.inflate(getContext(), R.layout.float_menu_view,null);//找到xml文件样式... ... addView(root);//将xml样式文件添加到这个View。也就是绑定。

那么进度球是如何显示到了菜单栏中?很简单,在菜单栏的xml布局文件中,也就是刚刚说的FloatMenuView 绑定的这个xml布局文件。在里面将其实例化

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="#33000000">    <LinearLayout        android:id="@+id/linearLayout"        android:orientation="vertical"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:background="#F02F3942"        android:layout_alignParentBottom="true"        android:clickable="true">        <!--左上角文本提示-->        <LinearLayout            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:orientation="horizontal">            <ImageView                android:layout_width="50dp"                android:layout_height="50dp"                android:layout_marginLeft="10dp"                android:layout_gravity="center_vertical"                android:src="@drawable/four"/>            <TextView                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:textSize="22sp"                android:textColor="#1296db"                android:text="四月加速球"                android:layout_marginLeft="10dp"                android:layout_gravity="center_vertical"                />        </LinearLayout><!--实例化自定义的进度球-->        <view.ProcessView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="center_horizontal"            android:layout_margin="10dp"/>    </LinearLayout></RelativeLayout>

8.1或者说是bug,或者说是技巧吧,在这里用到了。具体原因不是很清楚,以后有待查证。
当点击进度球(包括灰色区域)以外的地方,需要隐藏菜单栏(进度球是菜单栏的子控件)。但是点击事件监听的是整个父控件,也就是菜单栏。而点击进度球是有其自身动画效果需要实现的。
也就是子控件不需要因为父控件而给监听到。
这样解决:给进度球在xml文件或者通过代码的方式设置一个属性:clickable=”true”。
当给整个父控件设置了点击或者触摸的监听,如果不需要其子控件也给监听到(或者子控件的监听有额外的事件处理)时,给子控件设置属性:clickable=”true”。

最后,感谢你耐心的看完。水平有限,不足或者有误之处,请谅解!!!

源码已经开放在github上:https://github.com/My-Zzw/FloatView360Demo

原创粉丝点击