【Android】自定义View实现可展开FloatingBut
之前,项目中需要一个和界面风格匹配的课展开的悬浮按钮。在尝试了多个第三方库无果后,看起来只能自己写一个了。下面开始正题。
先丢一个效果图
首先,创建一个Myfab类,并继承自View类,然后复写初始的onMeasure方法以及构造方法
然后,分析一下这个按钮的绘制逻辑。
- 整个按钮背景由三部分组成,上部分的半圆,中间的矩形,以及下部分的半圆。
- 闭合的时候,中间矩形的高度为0,上下半圆贴在一起。
- 展开的过程中,下半部分半圆不动,矩形的长随着时间增长,上方半圆的y坐标随着长方形的增长而减小(就是被长方形顶上去了)。
- 绘制的时候判断,如果伸长的高度足够绘制下一个图标,就进行绘制。
然后就是代码
onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width;
int height;
width=MeasureSpec.getSize(widthMeasureSpec);
height=MeasureSpec.getSize(heightMeasureSpec);
width=width>height?height:width;//取较小的
height=height>width?width:height;
height+=icon.size()*width; //根据加入图标个数累加view高度
realheight=height-width;//加入图标的总高度
setMeasuredDimension(width,height);
}
这里,我取了用户设置的长和宽中较小的值作为button的直径,按钮展开后的高=直径*加入的图标个数,也就是说,每一个图标所占有的区域都是一个正方形。
onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
boolean result=false;
switch (event.getAction())
{
case MotionEvent.ACTION_UP:
{
result=TouchMethod((int)event.getX(), (int)event.getY(),false);
break;
}
case MotionEvent.ACTION_DOWN:
{
result=TouchMethod((int)event.getX(), (int)event.getY(),true);//
break;
}
}
if(result)
return true; //已消费事件
else
return false;//未消费事件
}
这里重写了onTouchEvent方法,以便处理view的点击事件。TouchMethod方法判断点击是否有效,如果有效,则消费点击事件,否则不消费。
TouchMethod
private boolean TouchMethod(int x,int y ,boolean isDown)
{
if(y>getMeasuredHeight()-getMeasuredWidth()&&y<getMeasuredHeight()) //如果点在底部按钮上
{
if(!isDown)
startAnimation();
return true;
}
else if(y>0&&y<getMeasuredHeight()-getMeasuredWidth()&&isShow)//如果点在选项上,并且按钮在展开状态
{
if(!isDown) {
for (int i = icon.size(); i > 0; i--) //计算并判断点在了哪个位置(view的宽度为Width,高度为icon.size*Width,相当于每个图标所占的区域都是正方形)
{
if (y > (i - 1) * getMeasuredWidth() && y < i * getMeasuredWidth()) {
if (menuListener != null)
menuListener.click(icon.size() - i + 1);//调用接口
}
}
}
return true;
}
else
return false;//按钮未展开
}
TouchMethod方法对view的点击事件进行了处理。如果按钮处于闭合状态,并且可见部分受到了点击,则展开菜单。如果不可见部分(收缩起来后上面添加的按钮部分)收到了点击,则会返回false,并由调用它的onTouchEvent方法返回未消费事件标记。如果按钮处于展开状态,并受到了点击,则会调用回调接口,并根据点击的区域传入相应的参数。
##onDraw
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
int px=getMeasuredWidth()/2;
Paint mPaint=new Paint();
mPaint.setColor(color); //设置画笔颜色
mPaint.setStyle(Paint.Style.FILL); //设置画笔模式为填充
mPaint.setStrokeWidth(10f);//设置画笔宽度为10px
mPaint.setAntiAlias(true);
Path path=new Path();
path.setFillType(Path.FillType.EVEN_ODD);
canvas.translate(px, getMeasuredHeight()-px);//移动坐标中心
canvas.drawArc(-px,-px,px,px,0,180,true,mPaint);//画出底部的半圆
canvas.drawArc(-px, -px - rect, px, -rect + px, 180, 180, true, mPaint); //画出上部分的半圆
canvas.drawRect(-px, -rect, px, 0, mPaint);//画出两个半圆中间的矩形
Bitmap bitmap= BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_add_circle_outline_white_24dp);
canvas.drawBitmap(bitmap,-bitmap.getWidth()/2,-bitmap.getHeight()/2,mPaint); //获取并绘制按钮没有展开时的图标
for(int i=0;i<icon.size();i++)
{
if(rect>=2*px*(i+1))//2*px=getMeasuredWidth(),i+1=当前的图标个数(-y),如果上升高度足够显示下一个图标,就绘制
{
Bitmap bitmap1= BitmapFactory.decodeResource(getContext().getResources(),icon.get(i).intValue());
canvas.drawBitmap(bitmap1,-bitmap.getWidth()/2,-bitmap.getHeight()/2-(i+1)*2*px,mPaint);//在相应位置绘制图标
}
}
if(rect==realheight) {//完全展开
isShow=true;
}
else if(rect==0) { //完全闭合
isShow=false;
}
}
首先,初始化画笔,画布等一系列东西。然后,onDraw会根据rect这个全局变量的值来进行绘制(rect的范围是0-realheight,大小的变化由自定义的Animation来控制,后面会有说明)。首先绘制的是两个半圆和半圆中间的矩形
canvas.drawArc(-px,-px,px,px,0,180,true,mPaint);//画出底部的半圆
canvas.drawArc(-px, -px - rect, px, -rect + px, 180, 180, true, mPaint); //画出上部分的半圆
canvas.drawRect(-px, -rect, px, 0, mPaint);//画出两个半圆中间的矩形
之后,绘制的是按钮在没有展开时显示在上面的图标
Bitmap bitmap= BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_add_circle_outline_white_24dp);
canvas.drawBitmap(bitmap,-bitmap.getWidth()/2,-bitmap.getHeight()/2,mPaint); //获取并绘制按钮没有展开时的图标
再根据动态插入图标的个数,绘制剩下的图标
for(int i=0;i<icon.size();i++)
{
if(rect>=2*px*(i+1))//2*px=getMeasuredWidth(),i+1=当前的图标个数(-y),如果上升高度足够显示下一个图标,就绘制
{
Bitmap bitmap1= BitmapFactory.decodeResource(getContext().getResources(),icon.get(i).intValue());
canvas.drawBitmap(bitmap1,-bitmap.getWidth()/2,-bitmap.getHeight()/2-(i+1)*2*px,mPaint);//在相应位置绘制图标
}
}
最后,再判断是否已经展开/回缩完全,并设置相应的flag即可。
控制rect变化的动画有两个,分别控制展开和回缩
控制展开的动画
private class ami extends Animation
{
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
rect=(int)(interpolatedTime*realheight);
invalidate();
}
}
控制回缩的动画
private class ami2 extends Animation
{
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
rect=(int)((1-interpolatedTime)*realheight);
invalidate();
}
}
以及对动画使用的控制
public void startAnimation() {
if(!isShow) {
ami move = new ami();
move.setDuration(300);
move.setInterpolator(new AccelerateDecelerateInterpolator());
startAnimation(move);
}
else
{
ami2 move = new ami2();
move.setDuration(300);
move.setInterpolator(new AccelerateDecelerateInterpolator());
startAnimation(move);
}
}
可以看出,想要展开/回缩button,只需要调用自定义的startAnimation()方法即可,动画持续的时间都是300毫秒。这里,我对动画设置了AccelerateDecelerateInterpolator这个插值器,以便实现开始加速和结束减速的效果,不过因为这个插值器是用的cos函数来给出插值,所以离着MD风格动画要求的精细度还差不少,并且开始和结束的加速度是一样的(MD动画要求较快的加速和缓慢的减速)...等着有空,再重新自定义个插值器吧。
以下给出整个view的完整代码
public class Myfab extends View {
private boolean isShow=false;
private int rect=0;
private List<Integer> icon=new ArrayList();
private MenuListener menuListener;
private int color;
private int realheight=0;
public void setColor(int color) {
this.color = color;
}
public Myfab(Context context) {
super(context);
}
public Myfab(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
if (!isClickable()) {
setClickable(true);
}
color=ContextCompat.getColor(context,R.color.colorPrimary);
}
public void collapse()
{
rect=0;
invalidate();//不加动画直接缩回去
}
public void setIcon(List<Integer> list)
{
this.icon=list;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result=false;
switch (event.getAction())
{
case MotionEvent.ACTION_UP:
{
result=TouchMethod((int)event.getX(), (int)event.getY(),false);
break;
}
case MotionEvent.ACTION_DOWN:
{
result=TouchMethod((int)event.getX(), (int)event.getY(),true);//
break;
}
}
if(result)
return true; //已消费事件
else
return false;//未消费事件
}
private boolean TouchMethod(int x,int y ,boolean isDown)
{
if(y>getMeasuredHeight()-getMeasuredWidth()&&y<getMeasuredHeight()) //如果点在底部按钮上
{
if(!isDown)
startAnimation();
return true;
}
else if(y>0&&y<getMeasuredHeight()-getMeasuredWidth()&&isShow)//如果点在选项上,并且按钮在展开状态
{
if(!isDown) {
for (int i = icon.size(); i > 0; i--) //计算并判断点在了哪个位置(view的宽度为Width,高度为icon.size*Width,相当于每个图标所占的区域都是正方形)
{
if (y > (i - 1) * getMeasuredWidth() && y < i * getMeasuredWidth()) {
if (menuListener != null)
menuListener.click(icon.size() - i + 1);//调用接口
}
}
}
return true;
}
else
return false;//按钮未展开
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
int px=getMeasuredWidth()/2;
Paint mPaint=new Paint();
mPaint.setColor(color); //设置画笔颜色
mPaint.setStyle(Paint.Style.FILL); //设置画笔模式为填充
mPaint.setStrokeWidth(10f);//设置画笔宽度为10px
mPaint.setAntiAlias(true);
Path path=new Path();
path.setFillType(Path.FillType.EVEN_ODD);
canvas.translate(px, getMeasuredHeight()-px);//移动坐标中心
canvas.drawArc(-px,-px,px,px,0,180,true,mPaint);//画出底部的半圆
canvas.drawArc(-px, -px - rect, px, -rect + px, 180, 180, true, mPaint); //画出上部分的半圆
canvas.drawRect(-px, -rect, px, 0, mPaint);//画出两个半圆中间的矩形
Bitmap bitmap= BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_add_circle_outline_white_24dp);
canvas.drawBitmap(bitmap,-bitmap.getWidth()/2,-bitmap.getHeight()/2,mPaint); //获取并绘制按钮没有展开时的图标
for(int i=0;i<icon.size();i++)
{
if(rect>=2*px*(i+1))//2*px=getMeasuredWidth(),i+1=当前的图标个数(-y),如果上升高度足够显示下一个图标,就绘制
{
Bitmap bitmap1= BitmapFactory.decodeResource(getContext().getResources(),icon.get(i).intValue());
canvas.drawBitmap(bitmap1,-bitmap.getWidth()/2,-bitmap.getHeight()/2-(i+1)*2*px,mPaint);//在相应位置绘制图标
}
}
if(rect==realheight) {//完全展开
isShow=true;
}
else if(rect==0) { //完全闭合
isShow=false;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width;
int height;
width=MeasureSpec.getSize(widthMeasureSpec);
height=MeasureSpec.getSize(heightMeasureSpec);
width=width>height?height:width;//取较小的
height=height>width?width:height;
height+=icon.size()*width; //根据加入图标个数累加view高度
realheight=height-width;//加入图标的总高度
setMeasuredDimension(width,height);
}
public void setMenuListener(MenuListener menuListener) {
this.menuListener = menuListener;
}
private class ami extends Animation
{
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
rect=(int)(interpolatedTime*(realheight));
invalidate();
}
}
private class ami2 extends Animation
{
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
rect=(int)((1-interpolatedTime)*(realheight));
invalidate();
}
}
public void startAnimation() {
if(!isShow) {
ami move = new ami();
move.setDuration(300);
move.setInterpolator(new AccelerateDecelerateInterpolator());
startAnimation(move);
}
else
{
ami2 move = new ami2();
move.setDuration(300);
move.setInterpolator(new AccelerateDecelerateInterpolator());
startAnimation(move);
}
}
public interface MenuListener//需实现此接口以便接受点击事件
{
void click(int i);
}
}
使用范例
public class MainActivity extends AppCompatActivity implements Myfab.MenuListener {
private Myfab fabtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fabtn=(Myfab) findViewById(R.id.fab);
List<Integer> list=new ArrayList();
list.add(R.mipmap.ic_add_circle_outline_white_24dp);
list.add(R.mipmap.ic_add_circle_outline_white_24dp);
list.add(R.mipmap.ic_add_circle_outline_white_24dp);
fabtn.setIcon(list);
fabtn.setMenuListener(this);
}
@Override
public void click(int i) {
Toast.makeText(this,String.valueOf(i),Toast.LENGTH_SHORT).show();
}
}
希望可以对大家有所帮助。