您的位置:首页 > 其它

开源项目学习与分析系列——ArcMenu

2014-04-03 19:06 127 查看
从现在开始,我将写下由于项目而接触到的优秀的Android开源项目的学习理解。一来有助于自己的提高,方便以后的查阅;二来学习Android需要有开源的精神,和别人分享是很重要的。我现在对Android应用开发的理解还不深,希望能在这个过程中,迅速的成长起来。

废话不多说,先展示一下,ArcMenu & RayMenu的效果图:









RayMenu机制详解:

其涉及的知识,是ViewGroup的布局与Android动画。由于ArcMenu & RayMenu实质上是一样的,只是ArcMenu布局计算上稍显复复杂一些,故选择其RayMenu作为例子来讲,柿子还是得捡软的捏呀。

RayMenu的用到的几个文件如下:

RayLayout.java——用于控制弹出菜单的动画与布局

RayMenu.java——用于控制控件的逻辑,比如按下红色按钮后,弹出菜单等。

RotateAndTranslateAnimation.java——这只是个动画

ray_menu.xml——rayMenu的布局文件

关于它的使用方法,很简单,见MainActivity.java

下面,我就从我们使用的流程开始说,

1: setContentView(R.layout.main);

实例化:

使用setContentView(int)会解析xml生成真正的view类,在这个过程RayMenu被实例化了,我们看一下实例化过程的初始化的代码。

1: public class RayMenu extends RelativeLayout {
2:      private RayLayout mRayLayout;

3:
4:      private ImageView mHintView;

5:
6:      public RayMenu(Context context) {

7: super(context);
8:          init(context);

9: }
10:

11: public RayMenu(Context context, AttributeSet attrs) {
12:          super(context, attrs);

13: init(context);
14:      }

15:
16:      private void init(Context context) {

17: setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
18:          setClipChildren(false);

19:
20:          LayoutInflater li = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

21: li.inflate(R.layout.ray_menu, this);
22:

23: mRayLayout = (RayLayout) findViewById(R.id.item_layout);
24:

25: final ViewGroup controlLayout = (ViewGroup) findViewById(R.id.control_layout);
26:          controlLayout.setClickable(true);

27: controlLayout.setOnTouchListener(new OnTouchListener() {
28:

29: @Override
30:              public boolean onTouch(View v, MotionEvent event) {

31: if (event.getAction() == MotionEvent.ACTION_DOWN) {
32:                      mHintView.startAnimation(createHintSwitchAnimation(mRayLayout.isExpanded()));

33: mRayLayout.switchState(true);
34:                  }

35:
36:                  return false;

37: }
38:          });

39:
40:          mHintView = (ImageView) findViewById(R.id.control_hint);

41: }

RayMenu继承于RelativeLayout, 向其他视图类一样,先调用父类的构造函数,然后就是自己的init(),在这里设置自己的高度依赖自己的内容,宽度与父视图一样宽。setClipChildren(false);传递给子控件进行绘制的canvas不剪切,这可以保证,当子控件的动画效果超出本身布局范围时,依然可见。

然后就是将布局文件添加进来,设置红色按钮的监听事件。

将布局文件添加进来时,又发生了子控件的实例化,这里主要讲一下Raylayout类的实例化过程。

1: public class RayLayout extends ViewGroup {
2:

3: /**
4:       * children will be set the same size.

5: */
6:      private int mChildSize;

7:
8:      /* the distance between child Views */

9: private int mChildGap;
10:

11: /* left space to place the switch button */
12:      private int mLeftHolderWidth;

13:
14:      private boolean mExpanded = false;

15:
16:      public RayLayout(Context context) {

17: super(context);
18:      }

19:
20:      public RayLayout(Context context, AttributeSet attrs) {

21: super(context, attrs);
22:

23: if (attrs != null) {
24:           TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ArcLayout, 0, 0);

25: mChildSize = Math.max(a.getDimensionPixelSize(R.styleable.ArcLayout_childSize, 0), 0);
26:              a.recycle();

27:
28:           a = getContext().obtainStyledAttributes(attrs, R.styleable.RayLayout, 0, 0);

29: mLeftHolderWidth = Math.max(a.getDimensionPixelSize(R.styleable.RayLayout_leftHolderWidth, 0), 0);
30:              a.recycle();

31:
32:          }

33: }

这个就更简单了,获得一些xml中的属性,比如重要的mchildSize和mLeftHolderWidth。初始化的工作还是很简单的,当然activity中的setContent()函数中发生了很多事情,这个以后有机会再讲,但是通过这个过程,我们就将视图set到了window上了。开始等待着绘制的消息,关于绘制过程的详细过程推荐阅读,Android中View绘制流程以及invalidate()等相关方法分析

menusure过程:

RayMenu.onMeasure()会调用RayMenu.Measure()函数,进而执行onMeasure():

1: private static int computeChildGap(final float width, final int childCount, final int childSize, final int minGap) {
2:       return Math.max((int) (width / childCount - childSize), minGap);

3: }
4:

5: @Override
6:      protected int getSuggestedMinimumHeight() {

7: return mChildSize;
8:      }

9:
10:   @Override

11: protected int getSuggestedMinimumWidth() {
12:          return mLeftHolderWidth + mChildSize * getChildCount();

13: }
14:

15: @Override
16:      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

17: super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(getSuggestedMinimumHeight(), MeasureSpec.EXACTLY));
18:

19: final int count = getChildCount();
20:          mChildGap = computeChildGap(getMeasuredWidth() - mLeftHolderWidth, count, mChildSize, 0);

21:

22:       for (int i = 0; i < count; i++) {< /pre>

23: getChildAt(i).measure(MeasureSpec.makeMeasureSpec(mChildSize, MeasureSpec.EXACTLY),

24:                   MeasureSpec.makeMeasureSpec(mChildSize, MeasureSpec.EXACTLY));

25: }
26:      }


1. 调用View.onMeasure(),这一步会确定控件的宽度与父视图一样宽,因为RayMenu设置的宽度fill_parent,且RayLayout设置的也是fill_parent,导致这里measure的宽度为屏幕宽度,高度为getSuggestionMinimumHeight(),即mChildSize的高度。

2. 确定menu item之间的距离mChildGap

3. measure了menu item的大小,显然其高宽为mChildSize。

Layout过程:

过程与measure类似。

1: private static Rect computeChildFrame(final boolean expanded, final int paddingLeft, final int childIndex,
2:           final int gap, final int size) {

3: final int left = expanded ? (paddingLeft + childIndex * (gap + size) + gap) : ((paddingLeft - size) / 2);
4:

5: return new Rect(left, 0, left + size, size);
6:      }

7:
8:      @Override

9: protected void onLayout(boolean changed, int l, int t, int r, int b) {
10:       final int paddingLeft = mLeftHolderWidth;

11: final int childCount = getChildCount();
12:


13: for (int i = 0; i < childCount; i++) {< /pre>

14:           Rect frame = computeChildFrame(mExpanded, paddingLeft, i, mChildGap, mChildSize);

15: getChildAt(i).layout(frame.left, frame.top, frame.right, frame.bottom);
16:          }

17:
18:      }


这个过程直接影响着menu item的布局,可以看到当expand时会呈直线排列,否则,则其中心与红色按钮的中心的x值一样。

Draw过程:

调用父类的函数完成。这里不再赘述。

事件驱动:

再回到使用RayMenu的步骤上来,然后就是添加Menu Item和监听事件:

1: private static final int[] ITEM_DRAWABLES = { R.drawable.composer_camera, R.drawable.composer_music,
2:           R.drawable.composer_place, R.drawable.composer_sleep, R.drawable.composer_thought, R.drawable.composer_with };

3:
4:  final int itemCount = ITEM_DRAWABLES.length;


5: for (int i = 0; i < itemCount; i++) {< /pre>

6:              ImageView item = new ImageView(this);

7: item.setImageResource(ITEM_DRAWABLES[i]);
8:

9: final int position = i;
10:           rayMenu.addItem(item, new OnClickListener() {

11:
12:               @Override

13: public void onClick(View v) {
14:                   Toast.makeText(MainActivity.this, "position:" + position, Toast.LENGTH_SHORT).show();

15: }
16:              });// Add a menu item

17: }
18:      }


完成这些后,就等待着RayMenu的在屏幕上出现了,你会看到一个红色的button,但为什么是红色的button而不是menu Item中的一个,显然这个时候Menu Item与红色Button应该重合,这涉及到view Z-order,因为在ray_menu.xml中controlLayout出现在rayLayout之后,在绘制的时候,若两者有重叠,则后添加进来的view会出现在上面。

当我们点击红色Button后,然后便进行了消息事件的传递(Android源码分析-点击事件派发机制), 这后便调用了

1: controlLayout.setOnTouchListener(new OnTouchListener() {
2:

3: @Override
4:           public boolean onTouch(View v, MotionEvent event) {

5: if (event.getAction() == MotionEvent.ACTION_DOWN) {
6:                      mHintView.startAnimation(createHintSwitchAnimation(mRayLayout.isExpanded()));

7: mRayLayout.switchState(true);
8:               }

9:
10:               return false;

11: }
12:       });


这里我比较费解的是为甚要使用OntouchListener,就代码而言,应该是想不占用其他监听名额。不过这导致手指一按下就会弹出菜单。我们继续往下进入mRayLayout.switchState(true)的代码。

1: /**
2:    * switch between expansion and shrinkage

3: *
4:    * @param showAnimation

5: */
6:      public void switchState(final boolean showAnimation) {

7: if (showAnimation) {
8:           final int childCount = getChildCount();


9: for (int i = 0; i < childCount; i++) {< /pre>

10:               bindChildAnimation(getChildAt(i), i, 300);

11: }
12:       }

13:
14:       mExpanded = !mExpanded;

15:
16:          if (!showAnimation) {

17: requestLayout();
18:       }

19:
20:          invalidate();

21: }

1: private void onAllAnimationsEnd() {
2:       final int childCount = getChildCount();


3: for (int i = 0; i < childCount; i++) {< /pre>

4:           getChildAt(i).clearAnimation();

5: }
6:

7: requestLayout();
8:      }


给子类添加动画,什么动画呢?就是弹出和旋转动画。然后expand的值变化了,再然后重绘,这样同样会使得动画启动(Android 动画框架详解,第 1 部分 ),但不会导致重新layout,等到动画都结束后,会调用onAllAnimationsEnd,会调用requestlayout,这样会引起重新布局,便回到了前面讲到的onLayout函数。当menu打开了,我们点击了一下item则会调用item的监听函数。

1: private OnClickListener getItemClickListener(final OnClickListener listener) {
2:       return new OnClickListener() {

3:
4:           @Override

5: public void onClick(final View viewClicked) {
6:               Animation animation = bindItemAnimation(viewClicked, true, 400);

7: animation.setAnimationListener(new AnimationListener() {
8:

9: @Override
10:                   public void onAnimationStart(Animation animation) {

11:
12:                   }

13:
14:                   @Override

15: public void onAnimationRepeat(Animation animation) {
16:

17: }
18:

19: @Override
20:                      public void onAnimationEnd(Animation animation) {

21: postDelayed(new Runnable() {
22:

23: @Override
24:                           public void run() {

25: itemDidDisappear();
26:                              }

27: }, 0);
28:                   }

29: });
30:

31: final int itemCount = mRayLayout.getChildCount();

32:                  for (int i = 0; i < itemCount; i++) {< /pre>

33:                      View item = mRayLayout.getChildAt(i);

34:                      if (viewClicked != item) {

35: bindItemAnimation(item, false, 300);
36:                      }

37: }
38:

39: mRayLayout.invalidate();
40:                  mHintView.startAnimation(createHintSwitchAnimation(true));

41:
42:                  if (listener != null) {

43: listener.onClick(viewClicked);
44:                  }

45: }
46:          };

47: }

和上一个一样,添加动画,被点击的item和其他item会被添加不同的动画,当动画结束后便会调用itemDIdDisappear():

1: private void itemDidDisappear() {
2:       final int itemCount = mRayLayout.getChildCount();


3: for (int i = 0; i < itemCount; i++) {< /pre>

4:           View item = mRayLayout.getChildAt(i);

5: item.clearAnimation();
6:       }

7:
8:       mRayLayout.switchState(false);

9: }

还记得之前讲的setClipChildren(false); 被点击的item有一个放大的动画,若不设置这个属性,则导致放大效果的上下部分都看不到。这涉及到canvas的剪裁,默认的父控件在调用子控件的draw函数时会,剪裁canvas是其尺寸为分配给子控件的大小。你不妨将menu Item的大小和红色Button的大小设成一样。就会发现会有一些问题。

RayMenu的改进:

能够让其放在两侧:

RayMenu只能从左向右弹出,有时候不能适应屏幕布局。

当然实现的方式有很多种,但是都要解决两个问题,1. controlLayout的位置 2. 根据controlLayout的位置变换item的布局和动画

这里讲一个最简单的,也是最粗暴的。因为Item动画的设置,是根据子Item的布局位置来的。所以控制好Item布局就能够满足第2点。至于第1点,不能在xml中设置,因为使用的merge,将会被RelativeLayout替换掉,android:layout_gravity不会有什么作用。

RayMenu.java

1: public void setHolderSide(boolean right) {
2:       mRayLayout.setHolderSide(right);

3: LayoutParams lp = (LayoutParams) findViewById(R.id.control_layout).getLayoutParams();
4:       if (right) {

5: lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT, 0);
6:           lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);

7: } else {
8:           lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);

9: lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 0);
10:       }

11: }

RayLayout.java

1: private boolean mHolderSide = false;
2:

3: public void setHolderSide(boolean right) {
4:       mHolderSide = right;

5: }
6:

7: private Rect computeChildFrame(final boolean expanded, final int paddingLeft, final int childIndex,
8:           final int gap, final int size) {

9: int left = expanded ? (paddingLeft + childIndex * (gap + size) + gap) : ((paddingLeft - size) / 2);
10:       if (mHolderSide) {

11: left = getMeasuredWidth() - (left + size);
12:       }

13: return new Rect(left, 0, left + size, size);
14:      }


在你使用的java文件中添加

1: rayMenu.setHolderSide(true);

当Item很大时,1.关闭RayMenu后,红色Button不能挡住item 2.item被点击时,出现的放大效果时上下部分会被截断。

我们不是设置了RayMenu的setClipChildren(false);吗?为甚还会出现这种情况,因为这个设置只对RayMenu内部的canvas传递有效,RayMenu设置的高度为LayoutParams.WRAP_CONTENT,所以最大也就是Max(红色button的高度,item的size),当item动画的高度超过RayMenu的高度时就会出现这种情况。

针对第1个问题,有两种解决办法:1.当关闭Raymenu后会有重新布局的机会,这个时候可以控制其布局,这个不推荐,因为布局还涉及到动画等等一些问题。

2. 当关闭Raymenu后,简单粗暴的将rayLayout设置为invisible就可以了,在这里要注意的设置为invisible和gone会对动画产生很大的不同(我并不清楚具体原因,不过在viewGroup的drawchild()函数中应该能找到原因)。

RayLayout.java

1: public void switchState(final boolean showAnimation) {
2:       if (showAnimation) {

3: final int childCount = getChildCount();

4:           for (int i = 0; i < childCount; i++) {< /pre>

5: bindChildAnimation(getChildAt(i), i, 300);

6:           }

7: }
8:

9: mExpanded = !mExpanded;
10:

11: if (!showAnimation) {
12:           if (!mExpanded) {

13: setVisibility(GONE);
14:           }

15: requestLayout();
16:          }

17:
18:       invalidate();

19: }
20:

21: private void onAllAnimationsEnd() {
22:       final int childCount = getChildCount();


23: for (int i = 0; i < childCount; i++) {< /pre>

24:           getChildAt(i).clearAnimation();

25: }
26:

27: if (!mExpanded) {
28:           setVisibility(INVISIBLE);

29: }
30:       requestLayout();

31: }
32:

33: }

RayMenu.java

1: controlLayout.setOnTouchListener(new OnTouchListener() {
2:

3: @Override
4:           public boolean onTouch(View v, MotionEvent event) {

5: if (event.getAction() == MotionEvent.ACTION_DOWN) {
6:                      mHintView.startAnimation(createHintSwitchAnimation(mRayLayout.isExpanded()));

7: if (!mRayLayout.isExpanded()) {
8:                       mRayLayout.setVisibility(VISIBLE);

9: }
10:                   mRayLayout.switchState(true);

11: }
12:

13: return false;
14:           }

15: });

针对第2个问题,有两种解决办法:1. 将RayMenu的父控件也同样设置为setClipChildren(false); 2. 由于放大动画是发大2倍。

RayLayout.java

1: private Rect computeChildFrame(final boolean expanded, final int paddingLeft, final int childIndex,
2:           final int gap, final int size) {

3: int left = expanded ? (paddingLeft + childIndex * (gap + size) + gap) : ((paddingLeft - size) / 2);
4:       if (mHolderSide) {

5: left = getMeasuredWidth() - (left + size);
6:       }

7: int top = (getSuggestedMinimumHeight()-size)/2;
8:       return new Rect(left, top, left + size, top+size);

9: }
10:

11: @Override
12:   protected int getSuggestedMinimumHeight() {

13: return mChildSize * 2;
14:      }


第1种解决办法更好一些。

还有另外一些变态要求,比如当RayMenu关闭后,能够随便移动,即可以拖拽红色Button。并且还要满足前面的要求。

我写了一个例子,这里就不详细讲了。放一个连接http://pan.baidu.com/share/link?shareid=1437532240&uk=405092275
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐