先来看一张动态图
昨天跟着视频学了如何自定义View并做成仿360悬浮球与加速球的样式
可以看出来,做成的效果有:
- 点击按钮后退出Activity,呈现一个圆形的悬浮球,可以随意拖动并会自动依靠到屏幕一侧,且拖动时会变成一张图片
- 当点击悬浮球时,悬浮球隐藏,底部出现一个加速球,双击加速球时,呈现水量逐渐增高且波动幅度较小的效果,单击时波浪上下波动且幅度渐小
- 点击屏幕不包含底部加速球的部位,加速球会隐藏,悬浮球重新出现
要做出这么一个效果,需要两个自定义View与一个自定义ViewGroup
首先,需要先设计悬浮球View——FloatBall
简单起见,为FloatBall指定一个默认宽度和高度——150像素
然后在onDraw(Canvas canvas)
方法中,判断FloatBall是否正在被拖动isDrag,如果是,则绘制一张默认图片bitmap,否则则根据绘图函数绘制圆形与居中文本
/*** Created by ZY on 2016/8/10.* 悬浮球*/
public class FloatBall extends View {public int width = 150;public int height = 150;//默认显示的文本private String text = "50%";//是否在拖动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.GRAY);ballPaint.setAntiAlias(true);textPaint = new Paint();textPaint.setTextSize(25);textPaint.setColor(Color.WHITE);textPaint.setAntiAlias(true);textPaint.setFakeBoldText(true);Bitmap src = BitmapFactory.decodeResource(getResources(), R.drawable.ninja);//将图片裁剪到指定大小bitmap = Bitmap.createScaledBitmap(src, width, height, true);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(width, height);}@Overrideprotected 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();}
}
因为FloatBall是不存在于Activity中而在屏幕单独显示的,所以需要用WindowManager来添加View并显示
新建一个类,命名为ViewManager,用来总的管理View的显示与删除
私有化构造函数并采用单例模式
private static ViewManager manager;//私有化构造函数private ViewManager(Context context) {this.context = context;init();}//获取ViewManager实例public static ViewManager getInstance(Context context) {if (manager == null) {manager = new ViewManager(context);}return manager;}
ViewManager包含有显示与隐藏悬浮球与加速球的函数
//显示浮动小球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_TOAST;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_TOAST;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);}}
将悬浮球置于Service中开启,这样悬浮球就不那么容易被系统去除了
在onCreate()方法中调用showFloatBall()
public class StartFloatBallService extends Service {public StartFloatBallService() {}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.throw new UnsupportedOperationException("Not yet implemented");}@Overridepublic void onCreate() {ViewManager manager = ViewManager.getInstance(this);manager.showFloatBall();super.onCreate();}
}
此时,只要为MainActivity添加一个按钮,并设定当点击按钮后开启Service,此时即可看到屏幕显示了一个悬浮球
public void startService(View view) {Intent intent = new Intent(this, StartFloatBallService.class);startService(intent);finish();}
不过此时悬浮球还不支持拖动与点击,还需要为其添加OnTouchListener与OnClickListener
View.OnTouchListener touchListener = new View.OnTouchListener() {float startX;float startY;float tempX;float tempY;@Overridepublic 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() {@Overridepublic void onClick(View v) {windowManager.removeView(floatBall);showFloatMenu();floatMenu.startAnimation();}};floatBall.setOnTouchListener(touchListener);floatBall.setOnClickListener(clickListener);
加速球ProgressBall的设计较为复杂,需要用到贝塞尔曲线来呈现波浪效果,且单击双击的效果也需要分开呈现
同样是让ProgressBall继承于View
进度值的意义在于限制水面最终上升到的高度,即根据目标进度值与最大进度值的比例来决定水面高度
波浪总的起伏次数Count用于在单击加速球时,水面上下波动的次数
//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;