Android 自定义ViewGroup手把手教你实现ArcMenu
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/37567907
逛eoe发现这样的UI效果,感觉很不错,后来知道github上有这么个开源项目~~~~当然本篇不是教你如何使用这个开源项目,而是教你如何自己通过自定义ViewGroup写这样的效果,自定义ViewGroup也是我的痛楚,嘿嘿,希望以此可以抛砖引玉~~
效果图:
1、实现思路
通过效果图,会有几个问题:
a、动画效果如何实现
可以看出动画是从顶点外外发射的,可能有人说,那还不简单,默认元素都在定点位置,然后TraslateAnimation就好了;这样忽略了一点,就是TraslateAnimation虽然有动画效果,但是本质是不会改变按钮的位置,我们的按钮动画结束是要点击的;有人可能会说那使用属性动画,或者改变leftMagin,rightMagin;这样可能比较麻烦,其实我们可以默认让子菜单就已经在目标位置,然后GONE,当点击时还是用TraslateAnimation,把起始位置设为定点,终点位置就是我们隐藏的区域,动画结束VISIBLE.
b、如何确定位置呢?
这可能需要一点数学上的知识,我画了一张草图(冰天雪地,跪玻璃碴子求画下面这些图的工具):
每次会根据子菜单数量,算出a这个角度,然后通过sin , cos 分别算出每个子菜单的left , top ;
当然这是在左上的情况,如果在右上,则top还是和左上一致的,left则为 (屏幕宽度-左上算出的left) ;其他两个方位同理~
整体我通过自定义一个ViewGroup,这个ViewGroup中第一个子元素为点击的按钮(你可以随便布局,随便用什么控件),接下来的子元素我认为是菜单项。根据效果图,决定展开半径和显示的位置,让用户自己去定制。下面看具体实现:
2、自定义View的属性:
- <?xml version="1.0" encoding="utf-8"?>
- <resources>
- <attr name="position">
- <enum name="left_top" value="0" />
- <enum name="right_top" value="1" />
- <enum name="right_bottom" value="2" />
- <enum name="left_bottom" value="3" />
- </attr>
- <attr name="radius" format="dimension"></attr>
- <declare-styleable name="ArcMenu">
- <attr name="position" />
- <attr name="radius"/>
- </declare-styleable>
- </resources>
3、在自定义的ViewGroup中获取这些属性
Arcmenu.java
- /**
- * @author zhy
- */
- public class ArcMenu extends ViewGroup implements OnClickListener
- {
- private static final String TAG = "ArcMenu";
- /**
- * 菜单的显示位置
- */
- private Position mPosition = Position.LEFT_TOP;
- /**
- * 菜单显示的半径,默认100dp
- */
- private int mRadius = 100;
- /**
- * 用户点击的按钮
- */
- private View mButton;
- /**
- * 当前ArcMenu的状态
- */
- private Status mCurrentStatus = Status.CLOSE;
- /**
- * 回调接口
- */
- private OnMenuItemClickListener onMenuItemClickListener;
- /**
- * 状态的枚举类
- *
- * @author zhy
- *
- */
- public enum Status
- {
- OPEN, CLOSE
- }
- /**
- * 设置菜单现实的位置,四选1,默认右下
- *
- * @author zhy
- */
- public enum Position
- {
- LEFT_TOP, RIGHT_TOP, RIGHT_BOTTOM, LEFT_BOTTOM;
- }
- public interface OnMenuItemClickListener
- {
- void onClick(View view, int pos);
- }
- public ArcMenu(Context context)
- {
- this(context, null);
- }
- public ArcMenu(Context context, AttributeSet attrs)
- {
- this(context, attrs, 0);
- }
- /**
- * 初始化属性
- *
- * @param context
- * @param attrs
- * @param defStyle
- */
- public ArcMenu(Context context, AttributeSet attrs, int defStyle)
- {
- super(context, attrs, defStyle);
- // dp convert to px
- mRadius = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
- mRadius, getResources().getDisplayMetrics());
- TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
- R.styleable.ArcMenu, defStyle, 0);
- int n = a.getIndexCount();
- for (int i = 0; i < n; i++)
- {
- int attr = a.getIndex(i);
- switch (attr)
- {
- case R.styleable.ArcMenu_position:
- int val = a.getInt(attr, 0);
- switch (val)
- {
- case 0:
- mPosition = Position.LEFT_TOP;
- break;
- case 1:
- mPosition = Position.RIGHT_TOP;
- break;
- case 2:
- mPosition = Position.RIGHT_BOTTOM;
- break;
- case 3:
- mPosition = Position.LEFT_BOTTOM;
- break;
- }
- break;
- case R.styleable.ArcMenu_radius:
- // dp convert to px
- mRadius = a.getDimensionPixelSize(attr, (int) TypedValue
- .applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100f,
- getResources().getDisplayMetrics()));
- break;
- }
- }
- a.recycle();
- }
4、计算子元素的大小:
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
- {
- int count = getChildCount();
- for (int i = 0; i < count; i++)
- {
- // mesure child
- getChildAt(i).measure(MeasureSpec.UNSPECIFIED,
- MeasureSpec.UNSPECIFIED);
- }
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
5、确定子元素的位置:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b)
- {
- if (changed)
- {
- layoutButton();
- int count = getChildCount();
- /**
- * 设置所有孩子的位置 例如(第一个为按钮): 左上时,从左到右 ] 第2个:mRadius(sin0 , cos0)
- * 第3个:mRadius(sina ,cosa) 注:[a = Math.PI / 2 * (cCount - 1)]
- * 第4个:mRadius(sin2a ,cos2a) 第5个:mRadius(sin3a , cos3a) ...
- */
- for (int i = 0; i < count - 1; i++)
- {
- View child = getChildAt(i + 1);
- child.setVisibility(View.GONE);
- int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2)
- * i));
- int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2)
- * i));
- // childview width
- int cWidth = child.getMeasuredWidth();
- // childview height
- int cHeight = child.getMeasuredHeight();
- // 右上,右下
- if (mPosition == Position.LEFT_BOTTOM
- || mPosition == Position.RIGHT_BOTTOM)
- {
- ct = getMeasuredHeight() - cHeight - ct;
- }
- // 右上,右下
- if (mPosition == Position.RIGHT_TOP
- || mPosition == Position.RIGHT_BOTTOM)
- {
- cl = getMeasuredWidth() - cWidth - cl;
- }
- Log.e(TAG, cl + " , " + ct);
- child.layout(cl, ct, cl + cWidth, ct + cHeight);
- }
- }
- }
首先在layoutButton中对按钮位置就行设置,以及初始化点击事件;然后从第二个子元素开始为菜单项,分别设置其位置,计算的原理就是上面我画的草图,可以再去仔细看看,动手在纸上画一画。
- /**
- * 第一个子元素为按钮,为按钮布局且初始化点击事件
- */
- private void layoutButton()
- {
- View cButton = getChildAt(0);
- cButton.setOnClickListener(this);
- int l = 0;
- int t = 0;
- int width = cButton.getMeasuredWidth();
- int height = cButton.getMeasuredHeight();
- switch (mPosition)
- {
- case LEFT_TOP:
- l = 0;
- t = 0;
- break;
- case LEFT_BOTTOM:
- l = 0;
- t = getMeasuredHeight() - height;
- break;
- case RIGHT_TOP:
- l = getMeasuredWidth() - width;
- t = 0;
- break;
- case RIGHT_BOTTOM:
- l = getMeasuredWidth() - width;
- t = getMeasuredHeight() - height;
- break;
- }
- Log.e(TAG, l + " , " + t + " , " + (l + width) + " , " + (t + height));
- cButton.layout(l, t, l + width, t + height);
- }
这是定位Button的代码,此时的代码已经实现了定位,如果你把onLayout中childView.setVisibility(VISIBLE)。ArcMenu的整个控件的样子已经实现了,接下来就是点击事件,已经效果动画的实现了。
6、设置按钮点击事件
- /**
- * 为按钮添加点击事件
- */
- @Override
- public void onClick(View v)
- {
- mButton = findViewById(R.id.id_button);
- if (mButton == null)
- {
- mButton = getChildAt(0);
- }
- rotateView(mButton, 0f, 270f, 300);
- toggleMenu(300);
- }
- /**
- * 按钮的旋转动画
- *
- * @param view
- * @param fromDegrees
- * @param toDegrees
- * @param durationMillis
- */
- public static void rotateView(View view, float fromDegrees,
- float toDegrees, int durationMillis)
- {
- RotateAnimation rotate = new RotateAnimation(fromDegrees, toDegrees,
- Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
- 0.5f);
- rotate.setDuration(durationMillis);
- rotate.setFillAfter(true);
- view.startAnimation(rotate);
- }
- public void toggleMenu(int durationMillis)
- {
- int count = getChildCount();
- for (int i = 0; i < count - 1; i++)
- {
- final View childView = getChildAt(i + 1);
- childView.setVisibility(View.VISIBLE);
- int xflag = 1;
- int yflag = 1;
- if (mPosition == Position.LEFT_TOP
- || mPosition == Position.LEFT_BOTTOM)
- xflag = -1;
- if (mPosition == Position.LEFT_TOP
- || mPosition == Position.RIGHT_TOP)
- yflag = -1;
- // child left
- int cl = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
- // child top
- int ct = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));
- AnimationSet animset = new AnimationSet(true);
- Animation animation = null;
- if (mCurrentStatus == Status.CLOSE)
- {// to open
- animset.setInterpolator(new OvershootInterpolator(2F));
- animation = new TranslateAnimation(xflag * cl, 0, yflag * ct, 0);
- childView.setClickable(true);
- childView.setFocusable(true);
- } else
- {// to close
- animation = new TranslateAnimation(0f, xflag * cl, 0f, yflag
- * ct);
- childView.setClickable(false);
- childView.setFocusable(false);
- }
- animation.setAnimationListener(new AnimationListener()
- {
- public void onAnimationStart(Animation animation)
- {
- }
- public void onAnimationRepeat(Animation animation)
- {
- }
- public void onAnimationEnd(Animation animation)
- {
- if (mCurrentStatus == Status.CLOSE)
- childView.setVisibility(View.GONE);
- }
- });
- animation.setFillAfter(true);
- animation.setDuration(durationMillis);
- // 为动画设置一个开始延迟时间,纯属好看,可以不设
- animation.setStartOffset((i * 100) / (count - 1));
- RotateAnimation rotate = new RotateAnimation(0, 720,
- Animation.RELATIVE_TO_SELF, 0.5f,
- Animation.RELATIVE_TO_SELF, 0.5f);
- rotate.setDuration(durationMillis);
- rotate.setFillAfter(true);
- animset.addAnimation(rotate);
- animset.addAnimation(animation);
- childView.startAnimation(animset);
- final int index = i + 1;
- childView.setOnClickListener(new View.OnClickListener()
- {
- @Override
- public void onClick(View v)
- {
- if (onMenuItemClickListener != null)
- onMenuItemClickListener.onClick(childView, index - 1);
- menuItemAnin(index - 1);
- changeStatus();
- }
- });
- }
- changeStatus();
- Log.e(TAG, mCurrentStatus.name() +"");
- }
点击时,触发TanslateAnimation动画,从定点向外扩展,也给点击按钮添加了一个旋转动画,每个子菜单项同样添加了旋转动画,且如果用户设置回调,调用回调接口;设置子菜单的点击事件。整体就是点击然后动画效果~~
7、设置子菜单的点击事件
- /**
- * 开始菜单动画,点击的MenuItem放大消失,其他的缩小消失
- * @param item
- */
- private void menuItemAnin(int item)
- {
- for (int i = 0; i < getChildCount() - 1; i++)
- {
- View childView = getChildAt(i + 1);
- if (i == item)
- {
- childView.startAnimation(scaleBigAnim(300));
- } else
- {
- childView.startAnimation(scaleSmallAnim(300));
- }
- childView.setClickable(false);
- childView.setFocusable(false);
- }
- }
- /**
- * 缩小消失
- * @param durationMillis
- * @return
- */
- private Animation scaleSmallAnim(int durationMillis)
- {
- Animation anim = new ScaleAnimation(1.0f, 0f, 1.0f, 0f,
- Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
- 0.5f);
- anim.setDuration(durationMillis);
- anim.setFillAfter(true);
- return anim;
- }
- /**
- * 放大,透明度降低
- * @param durationMillis
- * @return
- */
- private Animation scaleBigAnim(int durationMillis)
- {
- AnimationSet animationset = new AnimationSet(true);
- Animation anim = new ScaleAnimation(1.0f, 4.0f, 1.0f, 4.0f,
- Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
- 0.5f);
- Animation alphaAnimation = new AlphaAnimation(1, 0);
- animationset.addAnimation(anim);
- animationset.addAnimation(alphaAnimation);
- animationset.setDuration(durationMillis);
- animationset.setFillAfter(true);
- return animationset;
- }
点击的菜单项变大且慢慢透明消失,未点击的菜单项缩小消失~有兴趣的可以改成自己喜欢的动画~
注:动画效果很多借鉴了eoe上那位仁兄的代码,这类动画也比较简单,就不多说了~
好了,剩下就是些getter,setter了~
8、布局文件:
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- xmlns:zhy="http://schemas.android.com/apk/res/com.example.zhy_arcmenu"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
- <com.example.zhy_arcmenu.ArcMenu
- android:id="@+id/id_arcmenu1"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- zhy:position="left_top"
- zhy:radius="130dp" >
- <RelativeLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/composer_button" >
- <ImageView
- android:id="@+id/id_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:src="@drawable/composer_icn_plus" />
- </RelativeLayout>
- <ImageView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:src="@drawable/composer_camera"
- android:tag="Camera" />
- <ImageView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:src="@drawable/composer_sun"
- android:tag="Sun" />
- <ImageView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:src="@drawable/composer_place"
- android:tag="Place" />
- <ImageView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:src="@drawable/composer_sleep"
- android:tag="Sleep" />
- </com.example.zhy_arcmenu.ArcMenu>
嗯,第一个元素为按钮,其他的都是菜单项了~~喜欢用代码的,也可以代码生成,我是比较喜欢布局文件中写这些玩意~
9、MainActivity
- package com.example.zhy_arcmenu;
- import android.app.Activity;
- import android.os.Bundle;
- import android.view.View;
- import android.view.Window;
- import android.widget.ImageView;
- import android.widget.Toast;
- import com.example.zhy_arcmenu.ArcMenu.OnMenuItemClickListener;
- public class MainActivity extends Activity
- {
- private ArcMenu mArcMenuLeftTop;
- @Override
- protected void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- mArcMenuLeftTop = (ArcMenu) findViewById(R.id.id_arcmenu1);
- //动态添加一个MenuItem
- ImageView people = new ImageView(this);
- people.setImageResource(R.drawable.composer_with);
- people.setTag("People");
- mArcMenuLeftTop.addView(people);
- mArcMenuLeftTop
- .setOnMenuItemClickListener(new OnMenuItemClickListener()
- {
- @Override
- public void onClick(View view, int pos)
- {
- Toast.makeText(MainActivity.this,
- pos + ":" + view.getTag(), Toast.LENGTH_SHORT)
- .show();
- }
- });
- }
- }
结束~~有任何意见欢迎指出~~
版权声明:本文为博主原创文章,未经博主允许不得转载。
Android 自定义ViewGroup手把手教你实现ArcMenu的更多相关文章
- Android 自己定义ViewGroup手把手教你实现ArcMenu
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/37567907 逛eoe发现这种UI效果,感觉非常不错,后来知道github上有这 ...
- android自定义viewgroup之我也玩瀑布流
先看效果图吧, 继上一篇<android自定义viewgroup实现等分格子布局>中实现的布局效果,这里稍微有些区别,每个格子的高度不规则,就是传说的瀑布流布局,一般实现这种效果,要么用第 ...
- Android开发之手把手教你写ButterKnife框架(三)
欢迎转载,转载请标明出处: http://blog.csdn.net/johnny901114/article/details/52672188 本文出自:[余志强的博客] 一.概述 上一篇博客讲了, ...
- Android开发之手把手教你写ButterKnife框架(二)
欢迎转载,转载请标明出处: http://blog.csdn.net/johnny901114/article/details/52664112 本文出自:[余志强的博客] 上一篇博客Android开 ...
- Android 自定义ViewGroup 实战篇 -> 实现FlowLayout
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38352503 ,本文出自[张鸿洋的博客] 1.概述 上一篇已经基本给大家介绍了如 ...
- android自定义viewgroup初步之一----抽屉菜单
转载请注明出处 http://blog.csdn.net/wingichoy/article/details/47832151 几天前在慕课网上看到鸿洋老师的 自定义卫星菜单,感觉很有意思,于是看完视 ...
- Android自定义ViewGroup
视图分类就两类,View和ViewGroup.ViewGroup是View的子类,ViewGroup可以包含所有的View(包括ViewGroup),View只能自我描绘,不能包含其他View. 然而 ...
- Android自定义ViewGroup,实现自动换行
学习<Android开发艺术探索>中自定义ViewGroup章节 自定义ViewGroup总结的知识点 一.自定义ViewGroup中,onMeasure理解 onMeasure(int ...
- android自定义viewgroup实现等分格子布局
先上效果图: 实现这样的效果: 一般的思路就是,直接写布局文件,用LinearLayout 嵌套多层子LinearLayout,然后根据权重layout_weight可以达到上面的效果 还有就是利用g ...
随机推荐
- java--加强之 jdk1.5简单新特性,枚举,注解
转载请申明出处:http://blog.csdn.net/xmxkf/article/details/9944041 Jdk1.51新特性(静态导入,可变参数,加强for循环,自动拆装箱) 08.ja ...
- LeetCode(47)-Reverse Bits
题目: Reverse bits of a given 32 bits unsigned integer. For example, given input 43261596 (represented ...
- AngularJs 指令directive之require
controller的用法分为两种情形,一种是require自定义的controller,由于自定义controller中的属性方法都由自己编 写,使用起来比较简单:另一种方法则是require An ...
- ORACLE复杂查询之连接查询
一.传统的连接查询 1.交叉连接:返回笛卡尔积 WHERE中限定查询条件,可以预先过滤掉掉不符合条件的记录,返回的只是两个表中剩余记录(符合条件的记录)的笛卡尔积. 2.内连接:参与连接的表地位平等, ...
- Gradle初探
(一):创建一个Gradle项目 1. 环境准备 1.1. 先上Gradle官网下载最新版的程序,地址:https://gradle.org/gradle-download/. 1.2. 配置环境变量 ...
- Partitions - Partition Storage Modes and Processing-MOLAP、ROLAP、HOLAP
https://docs.microsoft.com/en-us/sql/analysis-services/multidimensional-models-olap-logical-cube-obj ...
- AdminIII连接linux Postgresql过程中的几个小问题
1.postgresql.conf主配置文件中要配置postgresql绑定的IP,如果不设置,可能只绑定本地闭环地址:127.0.0.1,可以设定为0.0.0.0:就包括了一切IPv4地址 2.pg ...
- Django1.6版本的PG数据库定义手动升级
Django1.7以后添加了migration功能,数据库定义的升级完全实现自动化,之前是通过一个叫south的app来做的.这篇文章谈一下1.6下的手动更新升级. 1.table create和ta ...
- Java杂记9—NIO
前言 非阻塞IO,也被称之为新IO,它重新定义了一些概念. 缓冲buffer 通道 channel 通道选择器 BIO 阻塞IO,几乎所有的java程序员都会的字节流,字符流,输入流,输出流等分类就是 ...
- Java 8 基础教程 - Predicate
在Java 8中,Predicate是一个函数式接口,可以被应用于lambda表达式和方法引用.其抽象方法非常简单: /** * Evaluates this predicate on the giv ...