偶尔在知乎上看到 豌豆荚工作氛围和企业文化都比较好,有归属感,多写写博客,希望能被豌豆荚发现
。
一、功能介绍
1、简述
这是一个自定义菜单View,包含菜单按钮和子菜单列表。只是
基础呈现。
2、功能描述
a、支持与该菜单的各种交互,包括:单机、双击、上下左右滑动、长按等;
b、单次点击,可隐藏或显示子菜单列表;
c、长按,可托拽到任意位置;
d、拖动过程中,子菜单列表始终环绕在菜单按钮周围;
e、支持任意个子菜单,支持动态增加、减少子菜单数量;
f、菜单按钮View可随意更改。
3、动画演示
二、技术难点
1、自定义View;
2、子菜单位置的计算;
3、交互事件,使用OnGestureListener、GestureDetector类;
4、托拽过程中,子菜单排列位置的实时变化。
三、设计思路
1、自定义View
注意onMeasure、onDraw方法和自定义的notifyLocationSetChanged方法。
2、子菜单位置计算
子菜单排列有九种方式:
屏幕中间CENTER,顺时针排列;
在屏幕左边界上BOUNDARY_LEFT,顺时针排列;
在屏幕右边界上BOUNDARY_RIGHT,逆时针排列;
在屏幕顶部边界上BOUNDARY_TOP,逆时针排列;
在屏幕底部边界上BOUNDARY_BOTTOM,顺时针排列;
在屏幕左上角CORNER_LEFT_TOP,顺时针排列;
在屏幕左下角CORNER_LEFT_BOTTOM,顺时针排列;
在屏幕右上角CORNER_RIGHT_TOP,逆时针排列;
在屏幕右下角CORNER_RIGHT_BOTTOM,顺时针排列。
注:在本文中约定,在以菜单中心为原点二维坐标系中,x轴正方向上为起始0弧度,则Y轴正方向上为-PI/2弧度,x轴负方向上为起始-PI弧度。
这里推理方式都差不多,仅以 屏幕中间CENTER 情况为例分析:
此时,子菜单可以围绕菜单按钮一周,即2*PI,那么子菜单之间的夹角为unitRadian = 2*PI/(count - 1) (count为子菜单的数量),我们可以得到,第一个子菜单位置在Y轴负方向上,距离为radius,第二个子菜单在在顺时针方向,与X轴正方向的夹角为-PI+unitRadian,以此类推,就可以得到所有子菜单位置,从而得到各子菜单距离菜单中心点的位置,到此就得到了Location。
从上面推理,可以得到子菜单的几点重要属性:起始位置startRadian、单位夹角unitRadian、方向direction、半径radius。
3、交互事件
当然会想到GestureDetector类处理。
4、托拽功能
监听长按事件和Touch事件的Move事件,根据矢量位移和图片在屏幕中的位置,在move过程中使用void android.view.ViewGroup.layout(int l, int t, int r, int b)方法,可重绘layout的位置。
四、源码展示
源码包括五部分:
菜单接口ChildsMenu
自定义的菜单视图ChildsMenuLayout
主菜单位置信息Location
交互精灵 GestureSprite
ChildsMenu.java:
public interface ChildsMenu {
public Point DEFAULT_POSITION = new Point(0, 0);
public void showChilds(View[] view);
public void notifyLocationSetChanged();
}
ChildsMenuLayout.java:
public class ChildsMenuLayout extends RelativeLayout implements ChildsMenu {
public double radius = 140d;
private Point position = DEFAULT_POSITION;
private List<PointF> points;
private Context mContext;
private View[] views;
private LOCATION location;
private boolean isShow = false;
public ChildsMenuLayout(Context context) {
super(context);
// TODO Auto-generated constructor stub
mContext = context;
setWillNotDraw(false);
}
public ChildsMenuLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
mContext = context;
setWillNotDraw(false);
}
public ChildsMenuLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
mContext = context;
setWillNotDraw(false);
}
@Override
public void showChilds(View[] views) {
// TODO Auto-generated method stub
isShow = true;
this.views = views;
points = getDataPoints(views);
this.invalidate();
}
public void hideChilds() {
isShow = false;
this.invalidate();
}
private List<PointF> getDataPoints(View[] views) {
if (views == null) {
isShow = false;
return null;
}
Location locationOnScreen = new Location(mContext , this);
location = locationOnScreen.getLocation();
List<PointF> points = new ArrayList<PointF>();
int count = views.length;
double unitRadian = 0;
double startRadian = 0;
double endRadian = 0;
final int Clockwise = 1;//顺时针
final int Eastern = -1;//逆时针
int direction = Clockwise;
double temp;
switch (location) {
case BOUNDARY_BOTTOM:
temp =Math.PI/2 - Math.asin((locationOnScreen.bottom + radius)/radius);
unitRadian = 2*(Math.PI -temp)/ (count - 1);
direction = Clockwise;
startRadian = -Math.PI*3/2+temp;
break;
case BOUNDARY_LEFT:
temp =Math.PI/2 - Math.asin((locationOnScreen.left + radius)/radius);
unitRadian = 2*(Math.PI - temp) / (count - 1);
direction = Clockwise;
startRadian = -Math.PI + temp;
break;
case BOUNDARY_RIGHT:
temp =Math.PI/2 - Math.asin((locationOnScreen.right + radius)/radius);
unitRadian = 2*(Math.PI - temp) / (count - 1);
direction = Eastern;
startRadian = - temp;
break;
case BOUNDARY_TOP:
temp =Math.PI/2 - Math.asin((locationOnScreen.top + radius)/radius);
unitRadian = 2*(Math.PI -temp) / (count - 1);
direction = Eastern;
startRadian = -Math.PI/2 - temp;
break;
case CENTER:
unitRadian = Math.PI * 2 / count;
direction = Clockwise;
startRadian = -Math.PI;
break;
case CORNER_LEFT_BOTTOM:
startRadian = Math.asin((radius+locationOnScreen.left)/radius);
endRadian = Math.asin((radius+locationOnScreen.bottom)/radius);
unitRadian = (Math.PI / 2+startRadian+endRadian) / (count - 1);
direction = Clockwise;
startRadian = -Math.PI / 2 -startRadian;
break;
case CORNER_LEFT_TOP:
startRadian = Math.asin((radius+locationOnScreen.top)/radius);
endRadian = Math.asin((radius+locationOnScreen.left)/radius);
unitRadian = (Math.PI / 2+startRadian+endRadian) / (count - 1);
direction = Clockwise;
startRadian = -startRadian;
break;
case CORNER_RIGHT_BOTTOM:
startRadian = Math.asin((radius+locationOnScreen.right)/radius);
endRadian = Math.asin((radius+locationOnScreen.bottom)/radius);
unitRadian = (Math.PI / 2+startRadian+endRadian) / (count - 1);
direction = Eastern;
startRadian = -Math.PI / 2+startRadian;
break;
case CORNER_RIGHT_TOP:
startRadian = Math.asin((radius+locationOnScreen.top)/radius);
endRadian = Math.asin((radius+locationOnScreen.right)/radius);
unitRadian = (Math.PI / 2+startRadian+endRadian) / (count - 1);
direction = Eastern;
startRadian = -Math.PI + startRadian;
break;
default:
break;
}
for (int i = 0; i < count; i++) {
PointF pt = new PointF();
if (direction == Eastern) {
float offsetX = (float) (position.x + radius
* Math.cos(-i * unitRadian + startRadian));
float offsetY = (float) (position.y + radius
* Math.sin(-i * unitRadian + startRadian));
pt.set(offsetX, offsetY);
} else if (direction == Clockwise) {
float offsetX = (float) (position.x + radius
* Math.cos(i * unitRadian + startRadian));
float offsetY = (float) (position.y + radius
* Math.sin(i * unitRadian + startRadian));
pt.set(offsetX, offsetY);
}
points.add(pt);
}
return points;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
radius = this.getWidth() / 2 - 20;
position = new Point(this.getWidth() / 2, this.getHeight() / 2);
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
Paint paint = new Paint();
paint.setARGB(255, 207, 0, 112);
paint.setTextSize(30);
paint.setAntiAlias(true);
if (points != null && isShow) {
for (int i = 0; i < points.size(); i++) {
PointF pt = points.get(i);
canvas.drawText("" + i, pt.x, pt.y, paint);
}
}
}
@Override
public void notifyLocationSetChanged() {
// TODO Auto-generated method stub
Location locationOnScreen = new Location(mContext , this);
LOCATION temp = locationOnScreen.getLocation();
if(location== LOCATION.CENTER && temp == location){
return;
}
location = temp;
points = getDataPoints(views);
this.invalidate();
}
}
GestureSprite.java
public class GestureSprite implements OnTouchListener, OnGestureListener {
private TextView tv2;
private ChildsMenuLayout mLayout;
private Context mContext;
private GestureDetector detector = new GestureDetector(this);
private final int MODE_DRAG = 0x1000;
private final int MODE_ZOOM = MODE_DRAG + 1;
private final int MODE_DEFAULT = -1;
private int MODE = MODE_DEFAULT;
private PointF oldPosition;
private PointF delta;
private View[] childViews;
private LOCATION location;
//************onTouch start***********
private float x = 0, y = 0;
private int dx, dy;
private int left = 0, top = 0;
//************onTouch end***********
private boolean isShowMenu = false;
@SuppressLint("NewApi")
public GestureSprite(Context context , ChildsMenuLayout layout,TextView tv2){
mLayout = layout;
mContext = context;
this.tv2 = tv2;
}
// 用户轻触触摸屏,由1个MotionEvent ACTION_DOWN触发
@Override
public boolean onDown(MotionEvent e) {
// TODO Auto-generated method stub
System.out.println("onDown");
tv2.setText("轻触触摸屏 按下");
return false;
}
// 用户轻触触摸屏,尚未松开或拖动,由一个1个MotionEvent ACTION_DOWN触发
// 注意和onDown()的区别,强调的是没有松开或者拖动的状态
@Override
public void onShowPress(MotionEvent e) {
// TODO Auto-generated method stub
System.out.println("onShowPress");
tv2.setText("轻触触摸屏,尚未松开或拖动");
}
// 用户(轻触触摸屏后)松开,由一个1个MotionEvent ACTION_UP触发
@Override
public boolean onSingleTapUp(MotionEvent e) {
// TODO Auto-generated method stub
System.out.println("onSingleTapUp");
toggleMenu();
tv2.setText("轻触触摸屏后松开");
return false;
}
// 用户按下触摸屏,并拖动,由1个MotionEvent ACTION_DOWN, 多个ACTION_MOVE触发
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
// TODO Auto-generated method stub
System.out.println("onScroll");
tv2.setText("按下触摸屏,并拖动");
return false;
}
// 用户长按触摸屏,由多个MotionEvent ACTION_DOWN触发
@Override
public void onLongPress(MotionEvent e) {
// TODO Auto-generated method stub
System.out.println("onLongPress");
tv2.setText("长按触摸屏");
MODE = MODE_DRAG;
}
// 用户按下触摸屏、快速移动后松开,由1个MotionEvent ACTION_DOWN, 多个ACTION_MOVE, 1个ACTION_UP触发
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
// TODO Auto-generated method stub
float dx = e2.getX() - e1.getX();
float dy = e2.getY() - e1.getY();
if(dx > 0 && Math.abs(dx) > Math.abs(dy)){
System.out.println("onFling right");
tv2.setText("右滑");
}else if(dx < 0 && Math.abs(dx) > Math.abs(dy)){
System.out.println("onFling left");
tv2.setText("左滑");
}else if(dy > 0 && Math.abs(dy) > Math.abs(dx)){
System.out.println("onFling down");
tv2.setText("下滑");
}else if(dy < 0 && Math.abs(dy) > Math.abs(dx)){
tv2.setText("上滑");
System.out.println("onFling up");
}
return false;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
detector.onTouchEvent(event);
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: // 指点杆按下
// 将当前的坐标保存为起始点
x = event.getRawX();
y = event.getRawY();
left = mLayout.getLeft();
top = mLayout.getTop();
break;
case MotionEvent.ACTION_MOVE: // 指点杆保持按下,并且进行位移
if (MODE == MODE_DRAG) {
dx = (int) ((event.getRawX() -x) + left);
dy = (int) ((event.getRawY() -y) + top);
mLayout.layout(dx, dy, dx + mLayout.getWidth(), dy + mLayout.getHeight());
mLayout.notifyLocationSetChanged();
}
break;
case MotionEvent.ACTION_UP: // 指点杆离开屏幕
MODE = MODE_DEFAULT;
break;
case MotionEvent.ACTION_POINTER_UP: // 有手指头离开屏幕,但还有没离开的
break;
case MotionEvent.ACTION_POINTER_DOWN: // 如果已经有手指压在屏幕上,又有一个手指压在了屏幕上
break;
}
return true;
}
private void toggleMenu(){
isShowMenu = isShowMenu?closeMenu():openMenu();
}
private boolean openMenu(){
childViews = new View[6];
mLayout.showChilds(childViews);
return true;
}
private boolean closeMenu(){
// int count = mLayout.getChildCount();
// if(count > 1){
// mLayout.removeViews(1, count);
// mLayout.invalidate();
// }
mLayout.hideChilds();
return false;
}
private void setLocation(){
View menuBtn = mLayout.getChildAt(0);
}
Location.java:
public class Location {
private int screenWidth;
private int screenHeight;
private int viewWidth;
private int viewHeight;
public double left;
public double top;
public double bottom;
public double right;
private int code;
public enum LOCATION{
CENTER,BOUNDARY_LEFT,BOUNDARY_RIGHT,BOUNDARY_TOP,BOUNDARY_BOTTOM,CORNER_LEFT_TOP,CORNER_LEFT_BOTTOM
,CORNER_RIGHT_TOP,CORNER_RIGHT_BOTTOM
}
public Location(Context context,View view){
screenWidth = ((Activity)context).getWindowManager().getDefaultDisplay().getWidth();
screenHeight = ((Activity)context).getWindowManager().getDefaultDisplay().getHeight();
viewWidth = view.getWidth();
viewHeight = view.getHeight();
int[] array = new int[2];
view.getLocationOnScreen(array);
left = array[0];
top = array[1];
right = screenWidth - (left + viewWidth);
bottom = screenHeight - (top + viewHeight);
}
private void onLeft(){
code = left < 0 ? 0x0001 : 0x0;
}
private void onRight(){
code = right < 0 ? code + 0x0010 : code;
}
private void onTop(){
code = top < 0 ? code + 0x0100 : code;
}
private void onBottom(){
code = bottom < 0 ? code + 0x1000 : code;
}
// private void onCornerLeftTop(){
// code = onLeft()&&onTop() ? code + 1 : code;
// }
//
// private void onCornerLeftBottom(){
// code = onLeft()&&onBottom();
// }
//
// private void onCornerRightTop(){
// code = onRight()&&onTop();
// }
//
// private void onCornerRightBottom(){
// code = onRight()&&onBottom();
// }
public LOCATION getLocation(){
LOCATION location = null;
onLeft();
onRight();
onTop();
onBottom();
switch (code) {
case 0x0000:
location = LOCATION.CENTER;
break;
case 0x0001:
location = LOCATION.BOUNDARY_LEFT;
break;
case 0x0010:
location = LOCATION.BOUNDARY_RIGHT;
break;
case 0x0100:
location = LOCATION.BOUNDARY_TOP;
break;
case 0x1000:
location = LOCATION.BOUNDARY_BOTTOM;
break;
case 0x1001:
location = LOCATION.CORNER_LEFT_BOTTOM;
break;
case 0x0101:
location = LOCATION.CORNER_LEFT_TOP;
break;
case 0x1010:
location = LOCATION.CORNER_RIGHT_BOTTOM;
break;
case 0x0110:
location = LOCATION.CORNER_RIGHT_TOP;
break;
default:
break;
}
return location;
}
}
xml:
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<com.example.template.sprite.ChildsMenuLayout
android:id="@+id/layout_bb_menu"
android:layout_width="150sp"
android:layout_height="150sp"
android:layout_centerInParent="true" >
<ImageView
android:id="@+id/btn_bb_menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:longClickable="true"
android:scaleType="matrix"
android:src="@drawable/ic_launcher" />
</com.example.template.sprite.ChildsMenuLayout>
</RelativeLayout>
调用时,只需一行代码:
menuBtn = (ImageView) findViewById(R.id.btn_bb_menu);
menuBtn.setOnTouchListener(new GestureSprite(this, menuLayout,tv2));
请标明转载自:http://blog.csdn.net/toyuexinshangwan/article/details/37594329
源码下载地址:http://download.csdn.net/detail/toyuexinshangwan/7613097