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

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

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

preview1 68747470733a2f2f646c2e64726f70626f7875736572636f6e74656e742e636f6d2f752f31313336393638372f70726576696577302e706e67 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。

你可能感兴趣的:(开源项目学习与分析系列——ArcMenu)