从现在开始,我将写下由于项目而接触到的优秀的Android开源项目的学习理解。一来有助于自己的提高,方便以后的查阅;二来学习Android需要有开源的精神,和别人分享是很重要的。我现在对Android应用开发的理解还不深,希望能在这个过程中,迅速的成长起来。
废话不多说,先展示一下,ArcMenu & 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()等相关方法分析。
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。
过程与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值一样。
调用父类的函数完成。这里不再赘述。
再回到使用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 item17: }
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);
我们不是设置了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。