Android自定义控件之钟摆菜单
来源:互联网 发布:mac cad安装教程 编辑:程序博客网 时间:2024/04/28 07:37
以前在搞一个私人小项目,对方UI设计了一个钟摆菜单,在github找了一圈,没有满意的,自己动手丰衣足食嘛,
最后的效果(如下图):
首先分析一下钟摆菜单需要实现哪些功能:随机自由摆动、简单的碰撞检测、菜单项点击事件
现在就来一步一步的实现上面的功能:(绘制界面->界面实现自由钟摆->界面实现碰撞检测->实现菜单项点击)
一、绘制界面
Android studio中新建项目,然后新建菜单控件基类PendulumMenu,并且继承View(基于主线程更新画面)
注:其实此处可以继承SurfaceView(基于新的线程更新画面),此处暂用View
在values目录下新建PendulumMenu.xml,作为自定义控件的自定义属性,代码如下:
<?xml version="1.0"encoding="utf-8"?><resources><declare-styleable name="PendulumMenu"><!--摆动因子即为角度变化基本单位--><attr name="speed"format="float"></attr><!--摆动速度与摆动因子相关--><attrname="speedduration" format="integer"></attr><!--子菜单圆图的大小--><attrname="circlesize" format="dimension"></attr><!--线条长度--><attr name="stroke"format="dimension"></attr><!--线条长度--><attrname="strokesize" format="dimension"></attr></declare-styleable><!--定义资源引用,用于主题资源进行默认值的赋值--><attr name="PendulumMenuDefalut" format="reference"></attr><!--定义主题资源引用时的默认值--><style name="PendulumMenuDefalutValues"> <item name="speedduration">20</item> <item name="circlesize">50dp</item> <item name="stroke">100dp</item> <item name="strokesize">2dp</item> <item name="graph">circle</item> </style></resources>
PendulumMenu类主要代码如下(包括自定义属性值的获取):
public class PendulumMenu extends View { private float speed = 0.3f;private int speedduration = 20;private int graph = 0;private int circlesize = 0;//图形尺寸(正方形)private int stroke = 0;//线条长度private int strokesize = 0;//线条宽度 public PendulumMenu(Context context) { this(context, null); } public PendulumMenu(Context context,AttributeSet attrs) { this(context, attrs,R.attr.PendulumMenuDefalut); }public PendulumMenu(Context context, AttributeSetattrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray tp=context.obtainStyledAttributes(attrs,R.styleable.PendulumMenu); speed =tp.getFloat(R.styleable.PendulumMenu_speed, speed); graph =tp.getInt(R.styleable.PendulumMenu_graph, 0); speedduration =tp.getInt(R.styleable.PendulumMenu_speedduration, speedduration); circlesize =tp.getDimensionPixelOffset(R.styleable.PendulumMenu_circlesize, 20); stroke =tp.getDimensionPixelOffset(R.styleable.PendulumMenu_stroke, 20); strokesize = tp.getDimensionPixelOffset(R.styleable.PendulumMenu_strokesize,2); //必须释放 tp.recycle();}}
接着来实现PendulumMenu控件的onMeasure宽高测量:
代码如下
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthmode = MeasureSpec.getMode(widthMeasureSpec); width = MeasureSpec.getSize(widthMeasureSpec); int heightmode = MeasureSpec.getMode(heightMeasureSpec); height = MeasureSpec.getSize(heightMeasureSpec); //不为精确匹配时直接赋值屏幕宽度 if (widthmode != MeasureSpec.EXACTLY) { width = getScreenWidth(); } else//精确模式时,取内部图形总宽度与控件宽度之间的最大值 width = width > circlesize * getChildCount() ? width : circlesize * getChildCount(); //高度计算(精确模式时,取内部图形与控件高度之间的最大值) if (heightmode == MeasureSpec.EXACTLY) height = height > (circlesize + stroke) ? height : (circlesize + stroke); else//高度为不精确模式时,取内部图形高度 height = circlesize + stroke;//(子菜单(圆形)直径+摆线长度) setMeasuredDimension(width, height);}
控件自身的宽高测量设置完成以后,由于存在多个子菜单,所以针对于子菜单创建一个子菜单实例类,代码 如下
public class CircleCollision { private int iniX;//初始化时记录的坐标 private int iniY;//初始化时最起初的Y坐标 private float centerX; private float centerY; private float radius;//自身半径bitmap图形 private float tickradius;//钟摆半径 private float conheight; private float conwidth; private double radians;//当前度数 private boolean right;//向右摆动 private Bitmap bitmap;//当前图形}
代码如下
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); for (int i = 0; i < getChildCount(); i++) {// getPoint(i)后面会贴出代码详细解释,此处只需要知道根据子项索引返回子菜单实例即可 CircleCollision pp = getPoint(i); canvas.drawLine(pp.getIniX(), pp.getIniY(), pp.getCenterX(), pp.getCenterY(), getLinePaint(i)); canvas.drawBitmap(pp.getBitmap(), pp.getCenterX() - circlesize / 2, pp.getCenterY() - circlesize / 2, new Paint()); }}
上面geiPoint()方法可以暂时不用更多纠结,暂时理解返回子menu的对应CircleCollision实例类即可。
此时已经完全实现了界面的显示,只是界面是静止的,现在就开始实现随机摆动,说一下思路:由于是使用Draw进行子menu的绘制,所以只需要变更绘画时子menu在PendulumMenu控件中XY坐标,从而计算出对应的left、top值进行绘制bitmap,重复调用Draw可以通过重复调用invalidate()来实现,这样修改子menu坐标的同时调用invalidate()进行界面刷新,此时子menu就在界面中实现了摆动效果。
主要代码如下(里面包含了碰撞的检测,此处只需要了解重复调用Draw刷新界面的原理即可,注意红色部分,注册一个Handler ,通过重复调用Handler实现):
private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg.what == 1) { invalidate(); if (collision == null) collision = CircleCollisionTesting.getInstance();//碰撞检测监听 collision.setonCollisionListener(new CircleCollisionTesting.onCollisionListener() { @Override public void onCollision(int index, boolean isCollision) { if (isCollision)//进行了碰撞 collisionOprate(index); } });//设置监听列表数据源 collision.setCollisionList(listc); handler.sendEmptyMessageDelayed(1, speedduration); } } };
现在子menu能够实现自由摆动了,那么现在就进行碰撞检测的实现。
二、碰撞检测
原理,menu与menu之间实现碰撞,可以通过两个menu中心点的距离是否大于两个menu的半径之和,若是大于则未碰撞,反之则碰撞。采用对每个menu进行的遍历来实现的,新建CircleCollisionTesting碰撞检测类,主要代码如下:
/** * 碰撞检测 * Created by Brian on 2016-10-17. */public class CircleCollisionTesting { private SparseArray<CircleCollision> listc; private onCollisionListener monCollisionListener; public static CircleCollisionTesting getInstance() { return new CircleCollisionTesting(); } private CircleCollisionTesting() { } public void setCollisionList(SparseArray<CircleCollision> listc) { this.listc = listc; startCollisionTesting(); } /** * 开始检测 */ public void startCollisionTesting() { if (listc == null) return; for (int i = 0; i < listc.size(); i++) { //进行边界检测 boolean isside = listc.get(i).getCenterX() <= listc.get(i).getRadius() || listc.get(i).getCenterY() <= listc.get(i).getRadius() || (listc.get(i).getConwidth() - listc.get(i).getCenterX()) <= listc.get(i).getRadius() || (listc.get(i).getConheight() - listc.get(i).getCenterY()) <= listc.get(i).getRadius(); if (monCollisionListener != null) { monCollisionListener.onCollision(i, isside); } for (int j = 0; j < listc.size(); j++) { if (i == j) continue; float x = listc.get(i).getCenterX() - listc.get(j).getCenterX(); float y = listc.get(i).getCenterY() - listc.get(j).getCenterY(); float c = listc.get(i).getRadius() + listc.get(j).getRadius(); if (monCollisionListener != null) { //进行碰撞的检测机制 monCollisionListener.onCollision(j, x * x + y * y <= c * c);//第二个参数即为是否进行了碰撞 } } } } public void setonCollisionListener(onCollisionListener monCollisionListener) { this.monCollisionListener = monCollisionListener; } public interface onCollisionListener { /** * 碰撞回调 * * @param index 修改索引 * @param isCollision 是否碰撞 */ void onCollision(int index, boolean isCollision); }}这样在PendulumMenu类中的Handler如上代码进行碰撞的检测的注册监听,然后collisionOprate(i)中对碰撞项进行相关操作,
代码如下(备注很详细):
/** * 碰撞检测操作 */ private void collisionOprate(int index) { //排除刚好位于最大角度时,此时的摆动标志right会自动更改(若此处在人为进行修改,则会出现角度出现大于degress的情况) if (index > listc.size() || index == -1 || Math.abs(listc.get(index).getRadians()) >= degress) return;//操作索引判定 //确定为碰撞后,更改图形摆动方向 listc.get(index).setRight(!listc.get(index).isRight()); }上面已经涉及到修改menu项的摆动方向setRight,那么现在就来解析,onDraw中getPoint(i)方法了,原理都是操作listc里面的子元素CircleCollision
类,主要代码如下:
/** * 根据子菜单索引获取对应的坐标 * * @param index * @return */ private CircleCollision getPoint(int index) { CircleCollision pp; //对当前控件进行缓存处理 if (listc.get(index) == null) { pp = new CircleCollision(); Bitmap bmp = getBitmap(arrres.get(index));//此处为根据用户设定的图片资源 pp.setBitmap(bmp); //不摆动时的垂直位置 pp.setIniX(getXLocation(index)); pp.setCenterX(getXLocation(index)); pp.setIniY(5); pp.setCenterY(getRealLineSize() + 5); Random randow = new Random(); pp.setTickradius(getRealLineSize());// pp.setTickradius(randow.nextInt(getRealLineSize() / 2) + getRealLineSize() / 2);//随机钟摆半径(介于最大钟摆半径~与一半钟摆半径之间) //随机定向摆动 pp.setRight(randow.nextInt(2) == 1);//产生0,1来进行随机左右摇摆 Log.e("setRight", pp.isRight() + ""); //每个索引项对应生成一个初期的随机角度(尚未转换为弧度)(介于正负degress区间) pp.setRadius(circlesize / 2); pp.setRadians(0);//(double) (randow.nextInt(degress * 2) - degress); pp.setConwidth(width); pp.setConheight(height); listc.put(index, pp); } else//获取缓存信息 pp = listc.get(index); if (pp.isRight())//向右 pp.setRadians(pp.getRadians() + speed); else pp.setRadians(pp.getRadians() - speed); if (Math.abs(pp.getRadians()) >= degress)//没有摆动到最大角度 pp.setRight(!pp.isRight()); //根据角度计算xy当前坐标 pp.setCenterX(pp.getIniX() + Math.round((float) (Math.sin(Math.toRadians(pp.getRadians())) * pp.getTickradius()))); pp.setCenterY(pp.getIniY() + Math.round((float) (Math.cos(Math.toRadians(pp.getRadians())) * pp.getTickradius()))); return pp; }
此处代码注意pp.setCenterX(pp.getIniX() + Math.round((float) (Math.sin(Math.toRadians(pp.getRadians())) * pp.getTickradius())));
最后来实现menu的点击功能。
三、Menu点击
原理:只需要判定手指点击/抬起时的坐标是否在某一个子menu内即可,
主要代码如下(PendulumMenu里面重载onTouchEvent):
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { if (monMenuItemListener != null) monMenuItemListener.onMenuClick(getTouchItem(event.getX(), event.getY())); } //此处只有直接返回true才能对手指抬起的坐标点进行获取 return true; }/** * 根据传入的xy坐标返回对应的点击项 * * @param touchx * @param touchy * @return */ private int getTouchItem(float touchx, float touchy) { int index = -1; for (int i = 0; i < listc.size(); i++) { float xdis = listc.get(i).getCenterX() - touchx; float ydis = listc.get(i).getCenterY() - touchy; //如果点击点位于bitmap的圆形区域内,则出发对应的点击事件(两点间距离进行判断) if (xdis * xdis + ydis * ydis <= listc.get(i).getRadius() * listc.get(i).getRadius()) { index = i; break; } } return index; }
这样就实现了子menu的点击事件,然后新建一个Activity进行新控件的演示,主要代码如下:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.yung.demo.MainActivity"> <com.yung.widget.PendulumMenu android:id="@+id/pendulummenuid" android:layout_width="match_parent" android:layout_height="match_parent" /></RelativeLayout>
public class MainActivity extends AppCompatActivity { PendulumMenu pendulummenuid; private int[] imgRes; private int[] linecos; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ini(); } public void ini() { pendulummenuid = (PendulumMenu) findViewById(R.id.pendulummenuid); imgRes = new int[]{R.mipmap.a, R.mipmap.b, R.mipmap.c, R.mipmap.d, R.mipmap.e}; linecos = new int[]{Color.parseColor("#ffbe00"), Color.parseColor("#ff9642"), Color.parseColor("#a8e968"), Color.parseColor("#63d4fe"), Color.parseColor("#ff8383")}; pendulummenuid.setTextsAndImages(imgRes, linecos); pendulummenuid.setonMenuItemListener(new PendulumMenu.onMenuItemListener() { @Override public void onMenuClick(int index) { if (index > -1)//不再bitmap点击区域内时,返回-1 Toast.makeText(MainActivity.this, "第" + (index + 1) + "个子菜单被点击", Toast.LENGTH_SHORT).show(); } }); pendulummenuid.start(); }}
综上:一个简单的钟摆菜单就实现了。其中注意事项:
1、自定义控件内部机制的了解,如invalidate()触发onDraw
2、利用三角函数进行坐标系的建立转换
3、碰撞的检测原理即是两点间距离,其实可以采用Region也能解决,此处只是为了简单处理。Region网上的使用方法有很多。
源代码地址:https://github.com/BrianYung/PendulumMenu
- Android自定义控件之钟摆菜单
- android自定义钟摆loadingView
- android 自定义控件之圆形菜单
- Android自定义控件之仿优酷菜单
- android控件-自定义菜单
- android 自定义菜单控件
- 自定义控件之仿优酷菜单
- Android开发之自定义控件-YouKu圆盘菜单
- android自定义控件之模仿优酷菜单
- Android自定义View: 如何实现类钟摆的动画效果?
- Android 动画 Tweened Animation 之 RotateAnimation钟摆动画
- Android 控件 之 Menu 菜单
- Android 控件 之 Menu 菜单
- Android 控件 之 Menu 菜单
- Android控件之菜单详解
- 自定义控件之优酷菜单
- 自定义控件之侧拉菜单SlidingMenu
- 自定义控件之侧滑菜单
- vue学习系列-demo04实现商城购物车功能
- 关于移动渐近线优化算法(MMA)的程序说明
- 封装数据库操作---数据实体操作封装(一)
- Move Zeroes
- 关于c++中模板的浅谈
- Android自定义控件之钟摆菜单
- Codeforces 410 div 2 【解题报告】
- C++文件读写详解(ofstream,ifstream,fstream)
- 《Cracking the Coding Interview程序员面试金典》----词频统计
- CCF NOI1087 第K名
- Java 8新特性
- 分治算法主定理
- 用servlet实现微博小程序----注册
- HDOJ 2021 发工资咯:)