精灵菜单

偶尔在知乎上看到 豌豆荚工作氛围和企业文化都比较好,有归属感,多写写博客,希望能被豌豆荚发现

一、功能介绍

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

精灵菜单

时间: 2024-08-25 22:49:16

精灵菜单的相关文章

通达OA 升级到2015精灵菜单异常的处理(图文)

升级到2015后,OA有全新的变化如上文所述,但是中间出现的菜单问题这里说一下. 安装完之后,从网页访问菜单都显示正常,只有个别人的菜单显示不全,如下图: 而且这个现象还很特别,一开始还考虑是不是跟机器安装的精灵有关系,把错误账号在精灵菜单正常的机器上登录显示还是异常,看来不是精灵的事情. 后经查找系统的菜单配置里的错误菜单提示,发现应用是工作流的菜单下,原来配置的工作流菜单与新系统里把原来在系统管理下的菜单都组织到了一起造成了菜单的混乱,找到问题就好办了,把工作流下的菜单重新进行调整后,菜单恢

Quick cocos2dx-Lua(V3.3R1)学习笔记(五)------创建精灵 菜单,让我们做个最简单的点击菜单显示精灵吧

开始第5篇笔记了,感觉前面的几篇写的太少了,Quick cocos2dx 前面加个quick,就是就能让我们快速上手,快速开发......balabala 一,我们来创建第一个精灵 今天我们来创建精灵,我们前面显示的外星人图片就是一个精灵. quick给我们提供了display.newSprite这个函数来创建精灵. 我们进入display.lua中看看对这个函数的介绍说明. 哇,这么长,我怎么知道那个函数定位在哪里(不是有搜索吗,不能Ctrl+F搜索么+_+). 我们换个方法,不用搜索,毕竟我

cocos2d-x实战 C++卷 学习笔记--第4章 使用菜单

前言: 菜单中包含菜单项,菜单项类是 MenuItem ,每个菜单项都有三个基本状态:正常.选中和禁止. (MenuItem)菜单分类: (文本菜单)MenuItemLabel : MenuItemAtlasFont, MenuItemFont (精灵菜单)MenuItemSprite : MenuItemImage (开关菜单)MenuItemToggle 文本菜单 文本菜单是 菜单项 只能显示文本,文本菜单类包括 MenuItemLabel.MenuItemFont 和 MenuItemAt

Cocos2d-JS中标签和菜单

一.标签 Cocos2d-JS中标签类重要有三种,cc.LabelTTF.cc.LabelAtlas和cc.LabelBMFont 1.cc.LabelTTF cc.LabelTTF是使用系统中的字体,它是最简单的标签类.cc.LabelTTF继承了cc.Node类,具有cc.Node的基本特性. 字体初始化代码如下: cc.LabelTTF类的构造函数定义如下 ctor(text,fontName,fontSize,dimensions,hAlignment,vAlignment) 2.cc.

《Cocos2d-x实战 C++卷》上线了-源码-样章-感谢大家的支持

<Cocos2d-x实战 C++卷>上线了 感谢大家一直以来的支持! 全面介绍Cocos开发技巧,采用Cocos2d-x3.2版本,并且详细介绍跨平台移植已经多平台发布细节. · 各大商店均开始销售: 京东:http://item.jd.com/11584534.html 亚马逊:http://www.amazon.cn/Cocos2d-x%E5%AE%9E%E6%88%98-C-%E5%8D%B7-%E5%85%B3%E4%B8%9C%E5%8D%87/dp/B00PTYWTLU 当当:ht

5.1-5.31推荐文章汇总

5.1-5.31推荐文章汇总 [移动开发] Android Volley完全解析(三),定制自己的Request guolin 雄踞AppStore榜首的游戏<别踩到白块儿>源代码分析和下载(一)touchsnow Cocos2d-x3.0游戏实例之<别救我>第四篇--乱入的主角笨木头 Android-自定义图像资源的使用(2)wwj_748 Android SQLite性能分析Horky <游戏脚本的设计与开发>-(RPG部分)3.6 队员列表和人物属性vipra C

cocos2d-x实战 C++卷 学习笔记--第6章 场景与层

前言: 一个场景(Scene)是由多个层(Layer)组成,而且层的个数要至少是1,不能为0. 场景切换 场景切换相关函数 1)void  runWithScene(Scene*  scene) 该函数可以运行场景.只能在启动第一个场景时调用该函数.如果已经有一个场景运行,则不能调用该函数. 2)void  replaceScene(Scene*  scene) 切换到下一个场景.用一个新的场景替换当前场景,当前场景被终端释放. 3)void  pushScene(Scene*  scene)

iOS_31_cocos2d_Menu

最终效果图: CCMenu继承自CCLayer,CCLayer继承自CCNode 下面是菜单项的继承结构图 从上面,可以看出,MenuItem是基类,不能使用, MenuItem的直接子类有3个:MenuItemLabel.MenuItemSprite.MenuItemToggle 其中,MenuItemLabel又有2个子类:MenuItemAtlasFont.MenuItemFont MenuItemSprite有一个更加方便使用的MenuItemImage,它可以分别定义normal和se

Cocos2d-x标签文乱码问题

我们在Windows下使用Visual Studio 2012开发游戏的时候,使用标签中包括中文时候会出现乱码或无法显示,例如以下图所看到的: 而应该显示的中文是例如以下图所看到的: HelloWorldScene.cpp中init函数例如以下: bool HelloWorld::init() { if( !Layer::init() ) { return false; } SizevisibleSize = Director::getInstance()->getVisibleSize();