由于项目需要,缺少一个类似于360悬浮窗的东西,在APP运行时以悬浮窗形式在手机显示,大家可以用来练习了解安卓自定义控件,自定义动画,手势触摸事件,以及窗体管理者在实际开发中的运用。
我大概要做这样的一个东西。
先说我最开始的思路:
总体思路:将浮窗的管理不写在Service里,而是交给Manager完成,在初始化Manager的时候,就初始化自定义浮窗(如图中两圆球),在里面写状态切换的方法,利用WindowManger通过当前的上下文来获取。分布的把Manger写个单例把自定义的浮窗写出来,然后再把整体组装到一起。
第一步:浮窗管理者(单例)的创建
- 私有化构造函数
- 创建静态的返回浮窗管理类的方法
第二步:浮窗小球的制作(自定义控件)
3. onMeasure用来确定该控件或者子控件的大小
4. onLayout用来确定该控件或者子控件在父窗体中的位置
5. onDraw用来画出该控件的内容
第三步:显示浮窗小球(WindowManager)
- addView()向当前的手机窗口界面添加View
- removeView()从当前窗体上移除View
- updateViewLayout()刷新当前View的显示
代码在第一步中的java文件中进行更改
到第三步为止,我就做不下去了,开始莫名奇妙的报错,先帝创业未半而中道崩溃,后来一查原因才知道原来是安卓8.0对“SYSTEM_ALERT_WINDOW
权限”进行了更改,并且新增了一种悬浮窗的窗口类型。
如果应用使用 SYSTEM_ALERT_WINDOW 权限并且尝试使用以下窗口类型之一来在其他应用和系统窗口上方显示提醒窗口,会炸掉的。。。
TYPE_PHONE TYPE_PRIORITY_PHONE TYPE_SYSTEM_ALERT TYPE_SYSTEM_OVERLAY TYPE_SYSTEM_ERROR TYPE_TOAST
就是上述这几个,真的会出问题,并且权限申请可能也要出现问题,我没有去申请动态权限(不是我懒,哼),只是加了
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"
tools:ignore="WrongManifestParent" />
以上是部分重点,接下来我贴出全部代码,减少各位下载时间,复制粘贴即可。
目录如图:
第一个代码文件MainActivity
package com.example.puber.myapplication; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Toast; import com.example.puber.myapplication.Service.*; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void startService(View view) { Intent intent = new Intent(this, StartFloatBallService.class); startService(intent); finish(); } }
ViewManager中的代码
package com.example.puber.myapplication.Manager; import android.content.Context; import android.graphics.PixelFormat; import android.graphics.Point; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import com.example.puber.myapplication.View.FloatBall; import com.example.puber.myapplication.View.FloatMenu; import java.lang.reflect.Field; /** * 管理者,单例模式 */ public class ViewManager { private FloatBall floatBall; private FloatMenu floatMenu; private WindowManager windowManager; private static ViewManager manager; private LayoutParams floatBallParams; private LayoutParams floatMenuParams; private Context context; //私有化构造函数 private ViewManager(Context context) { this.context = context; init(); } public void init() { windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); floatBall = new FloatBall(context); floatMenu = new FloatMenu(context); View.OnTouchListener touchListener = new View.OnTouchListener() { float startX; float startY; float tempX; float tempY; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startX = event.getRawX(); startY = event.getRawY(); tempX = event.getRawX(); tempY = event.getRawY(); break; case MotionEvent.ACTION_MOVE: float x = event.getRawX() - startX; float y = event.getRawY() - startY; //计算偏移量,刷新视图 floatBallParams.x += x; floatBallParams.y += y; floatBall.setDragState(true); windowManager.updateViewLayout(floatBall, floatBallParams); startX = event.getRawX(); startY = event.getRawY(); break; case MotionEvent.ACTION_UP: //判断松手时View的横坐标是靠近屏幕哪一侧,将View移动到依靠屏幕 float endX = event.getRawX(); float endY = event.getRawY(); if (endX < getScreenWidth() / 2) { endX = 0; } else { endX = getScreenWidth() - floatBall.width; } floatBallParams.x = (int) endX; floatBall.setDragState(false); windowManager.updateViewLayout(floatBall, floatBallParams); //如果初始落点与松手落点的坐标差值超过6个像素,则拦截该点击事件 //否则继续传递,将事件交给OnClickListener函数处理 if (Math.abs(endX - tempX) > 6 && Math.abs(endY - tempY) > 6) { return true; } break; } return false; } }; OnClickListener clickListener = new OnClickListener() { @Override public void onClick(View v) { windowManager.removeView(floatBall); showFloatMenu(); floatMenu.startAnimation(); } }; floatBall.setOnTouchListener(touchListener); floatBall.setOnClickListener(clickListener); } //显示浮动小球 public void showFloatBall() { if (floatBallParams == null) { floatBallParams = new LayoutParams(); floatBallParams.width = floatBall.width; floatBallParams.height = floatBall.height - getStatusHeight(); floatBallParams.gravity = Gravity.TOP | Gravity.LEFT; floatBallParams.type = LayoutParams.TYPE_APPLICATION_OVERLAY; floatBallParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL; floatBallParams.format = PixelFormat.RGBA_8888; } windowManager.addView(floatBall, floatBallParams); } //显示底部菜单 private void showFloatMenu() { if (floatMenuParams == null) { floatMenuParams = new LayoutParams(); floatMenuParams.width = getScreenWidth(); floatMenuParams.height = getScreenHeight() - getStatusHeight(); floatMenuParams.gravity = Gravity.BOTTOM; floatMenuParams.type = LayoutParams.TYPE_APPLICATION_OVERLAY; floatMenuParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL; floatMenuParams.format = PixelFormat.RGBA_8888; } windowManager.addView(floatMenu, floatMenuParams); } //隐藏底部菜单 public void hideFloatMenu() { if (floatMenu != null) { windowManager.removeView(floatMenu); } } //获取ViewManager实例 public static ViewManager getInstance(Context context) { if (manager == null) { manager = new ViewManager(context); } return manager; } //获取屏幕宽度 public int getScreenWidth() { Point point = new Point(); windowManager.getDefaultDisplay().getSize(point); return point.x; } //获取屏幕高度 public int getScreenHeight() { Point point = new Point(); windowManager.getDefaultDisplay().getSize(point); return point.y; } //获取状态栏高度 public int getStatusHeight() { try { Class<?> c = Class.forName("com.android.internal.R$dimen"); Object object = c.newInstance(); Field field = c.getField("status_bar_height"); int x = (Integer) field.get(object); return context.getResources().getDimensionPixelSize(x); } catch (Exception e) { return 0; } } }
StartFloatBallService中的代码
package com.example.puber.myapplication.Service; import android.app.Service; import android.content.Intent; import android.os.IBinder; import com.example.puber.myapplication.Manager.ViewManager; public class StartFloatBallService extends Service { public StartFloatBallService() { } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException("Not yet implemented"); } @Override public void onCreate() { ViewManager manager = ViewManager.getInstance(this); manager.showFloatBall(); super.onCreate(); } }
FloatBall中的代码
package com.example.puber.myapplication.View; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; import com.example.puber.myapplication.R; public class FloatBall extends View{ public int width = 150; public int height = 150; //默认显示的文本 private String text = "SOS"; //是否在拖动 private boolean isDrag; private Paint ballPaint; private Paint textPaint; private Bitmap bitmap; public FloatBall(Context context) { super(context); init(); } public FloatBall(Context context, AttributeSet attrs) { super(context, attrs); init(); } public FloatBall(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public void init() { ballPaint = new Paint(); ballPaint.setColor(Color.GREEN); ballPaint.setAntiAlias(true); textPaint = new Paint(); textPaint.setTextSize(25); textPaint.setColor(Color.BLACK); textPaint.setAntiAlias(true); textPaint.setFakeBoldText(true); Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.ninja); //将图片裁剪到指定大小 bitmap = Bitmap.createScaledBitmap(src, width, height, true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { if (!isDrag) { canvas.drawCircle(width / 2, height / 2, width / 2, ballPaint); float textWidth = textPaint.measureText(text); Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); float dy = -(fontMetrics.descent + fontMetrics.ascent) / 2; canvas.drawText(text, width / 2 - textWidth / 2, height / 2 + dy, textPaint); } else { //正在被拖动时则显示指定图片 canvas.drawBitmap(bitmap, 0, 0, null); } } //设置当前移动状态 public void setDragState(boolean isDrag) { this.isDrag = isDrag; invalidate(); } }
FloatMenu中的代码
package com.example.puber.myapplication.View; import android.content.Context; import android.view.MotionEvent; import android.view.View; import android.view.animation.Animation; import android.view.animation.TranslateAnimation; import android.widget.LinearLayout; import com.example.puber.myapplication.Manager.ViewManager; import com.example.puber.myapplication.R; /** * 底部菜单栏 */ public class FloatMenu extends LinearLayout { private LinearLayout layout; private TranslateAnimation animation; public FloatMenu(final Context context) { super(context); View root = View.inflate(context, R.layout.float_menu, null); layout = (LinearLayout) root.findViewById(R.id.layout); animation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 1.0f, Animation.RELATIVE_TO_SELF, 0); animation.setDuration(500); animation.setFillAfter(true); layout.setAnimation(animation); root.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { ViewManager manager = ViewManager.getInstance(context); manager.showFloatBall(); manager.hideFloatMenu(); return false; } }); addView(root); } public void startAnimation() { animation.start(); } }
ProgressBall中的代码
package com.example.puber.myapplication.View; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.os.Handler; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; import android.view.View; /** * 实现的效果为: * 双击后小球内的波浪高度从零增加到目标进度值targetProgress * 单击后波浪呈现上下浮动且波动渐小的效果 */ public class ProgressBall extends View { //view的宽度 private int width = 200; //view的高度 private int height = 200; //最大进度值 private final int maxProgress = 100; //当前进度值 private int currentProgress = 0; //目标进度值 private final int targetProgress = 70; //是否为单击 private boolean isSingleTop; //设定波浪总的起伏次数 private final int Count = 20; //当前起伏次数 private int currentCount; //初始振幅大小 private final int startAmplitude = 15; //波浪周期性出现的次数 private final int cycleCount = width / (startAmplitude * 4) + 1; private DoubleTapRunnable doubleTapRunnable = new DoubleTapRunnable(); private SingleTapRunnable singleTapRunnable = new SingleTapRunnable(); private Canvas bitmapCanvas; private Bitmap bitmap; private Path path; private Paint ballPaint; private Paint progressPaint; private Paint textPaint; private Context context; private Handler handler; private GestureDetector gestureDetector; public ProgressBall(Context context) { super(context); this.context = context; init(); } public ProgressBall(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; init(); } public ProgressBall(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; init(); } private void init() { //初始化小球画笔 ballPaint = new Paint(); ballPaint.setAntiAlias(true); ballPaint.setColor(Color.argb(0xff, 0x3a, 0x8c, 0x6c)); //初始化(波浪)进度条画笔 progressPaint = new Paint(); progressPaint.setAntiAlias(true); progressPaint.setColor(Color.argb(0xff, 0x4e, 0xc9, 0x63)); progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //初始化文字画笔 textPaint = new Paint(); textPaint.setAntiAlias(true); textPaint.setColor(Color.WHITE); textPaint.setTextSize(25); handler = new Handler(); path = new Path(); bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bitmapCanvas = new Canvas(bitmap); //手势监听 //重点在于将单击和双击操作分隔开 SimpleOnGestureListener listener = new SimpleOnGestureListener() { //双击 @Override public boolean onDoubleTap(MotionEvent e) { //当前波浪起伏次数为零,说明“单击效果”没有影响到现在 if (currentCount == 0) { //当前进度为零或者已达到目标进度值,说明“双击效果”没有影响到现在,此时可以允许进行双击操作 if (currentProgress == 0 || currentProgress == targetProgress) { currentProgress = 0; isSingleTop = false; startDoubleTapAnimation(); } } return super.onDoubleTap(e); } //单击 @Override public boolean onSingleTapConfirmed(MotionEvent e) { //当前进度值等于目标进度值,且当前波动次数为零,则允许进行单击操作 if (currentProgress == targetProgress && currentCount == 0) { isSingleTop = true; startSingleTapAnimation(); } return super.onSingleTapConfirmed(e); } }; gestureDetector = new GestureDetector(context, listener); setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return gestureDetector.onTouchEvent(event); } }); //接受点击操作 setClickable(true); } class DoubleTapRunnable implements Runnable { @Override public void run() { if (currentProgress < targetProgress) { invalidate(); handler.postDelayed(doubleTapRunnable, 50); currentProgress++; } else { handler.removeCallbacks(doubleTapRunnable); } } } //开启双击动作动画 public void startDoubleTapAnimation() { handler.postDelayed(doubleTapRunnable, 50); } class SingleTapRunnable implements Runnable { @Override public void run() { if (currentCount < Count) { invalidate(); currentCount++; handler.postDelayed(singleTapRunnable, 100); } else { handler.removeCallbacks(singleTapRunnable); currentCount = 0; } } } //开启单击动作动画 public void startSingleTapAnimation() { handler.postDelayed(singleTapRunnable, 100); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { //绘制圆形 bitmapCanvas.drawCircle(width / 2, height / 2, width / 2, ballPaint); path.reset(); //高度随当前进度值的变化而变化 float y = (1 - (float) currentProgress / maxProgress) * height; //属性PorterDuff.Mode.SRC_IN代表了progressPaint只显示与下层层叠的部分, //所以以下四点虽然连起来是个矩形,可呈现出来的依然是圆形 //右上角 path.moveTo(width, y); //右下角 path.lineTo(width, height); //左下角 path.lineTo(0, height); //左上角 path.lineTo(0, y); //绘制顶部波浪 if (!isSingleTop) { //是双击 //根据当前进度大小调整振幅大小,有逐渐减小的趋势 float tempAmplitude = (1 - (float) currentProgress / targetProgress) * startAmplitude; for (int i = 0; i < cycleCount; i++) { path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); } } else { //是单击 //根据当前次数调整振幅大小,有逐渐减小的趋势 float tempAmplitude = (1 - (float) currentCount / Count) * startAmplitude; //因为想要形成波浪上下起伏的效果,所以根据currentCount的奇偶性来变化贝塞尔曲线转折点位置 if (currentCount % 2 == 0) { for (int i = 0; i < cycleCount; i++) { path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); } } else { for (int i = 0; i < cycleCount; i++) { path.rQuadTo(startAmplitude, -tempAmplitude, 2 * startAmplitude, 0); path.rQuadTo(startAmplitude, tempAmplitude, 2 * startAmplitude, 0); } } } path.close(); bitmapCanvas.drawPath(path, progressPaint); String text = (int) (((float) currentProgress / maxProgress) * 100) + "%"; float textWidth = textPaint.measureText(text); Paint.FontMetrics metrics = textPaint.getFontMetrics(); float baseLine = height / 2 - (metrics.ascent + metrics.descent); bitmapCanvas.drawText(text, width / 2 - textWidth / 2, baseLine, textPaint); canvas.drawBitmap(bitmap, 0, 0, null); } }
activity_main.xml中代码:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:onClick="startService" android:text="开启悬浮球" /> </android.support.constraint.ConstraintLayout>
float_menu.xml中的代码:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:background="#556f7f8f" android:clickable="true" android:gravity="center" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="10dp" android:gravity="center" android:text="是否选择求助" /> <com.example.puber.myapplication.View.ProgressBall android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="20dp" /> </LinearLayout> </RelativeLayout> </android.support.constraint.ConstraintLayout>
以上是我这次的悬浮球的代码,写的心累,唉。