您的位置:首页 > 其它

Side-Menu源码分析讲解

2017-07-07 11:06 260 查看
Android的一份开源侧边菜单,看起来效果还不错,所以就看了一下源码。发现代码为MVP特点的处理方式。

连接地址https://github.com/Yalantis/Side-Menu.Android

展示



分析如下:

一、控件要求

a主界面布局需要使用 DrawerLayout 作为容器(DrawerLayout 用法这里不做讲解)

b基本界面使用了RevealFrameLayout作为界面更换的界面容器(一个有Reveal效果的开源控件,具有Android版本的

兼容作用),具体可见io.codetail.animation.ViewAnimationUtils中createCircularReveal方法

二、接口设计



ScreenShotable:
 

ContentFragment继承进行界面Bitmap的获取与返回

ViewAnimator.ViewAnimatorListener:

MainActivity主界面继承,设置动画播放时的其他操作ActionBar中Home按钮的状态处理,以及侧边菜单子项的点击

事件onSwitch的处理

三、流程讲解:

MainActivity主界面进入:见代码(具体都有注释)

    
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//进行界面的首次替换展示,菜单对应界面的初始化
contentFragment = ContentFragment.newInstance(R.drawable.content_music);
//界面展示
getSupportFragmentManager().beginTransaction()
.replace(R.id.content_frame, contentFragment)
.commit();
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
// 设置抽屉空余处颜色
drawerLayout.setScrimColor(Color.TRANSPARENT);
linearLayout = (LinearLayout) findViewById(R.id.left_drawer);
linearLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//关闭抽屉
drawerLayout.closeDrawers();
}
});
//此处进行ActionBar与DrawerLayout通过ActionBarDrawerToggle建立事件关系
setActionBar();
//侧滑菜单项目初始化(还不可见)
createMenuList();
//通过自定义ViewAnimator进行相关的事件监控
//分别传入菜单数据list、ContentFragment(实现ScreenShotable接口)、drawerLayout、当前MainActivity对象
viewAnimator = new ViewAnimator<>(this, list, contentFragment, drawerLayout, this);
}


主界面流程:
1、contentFragment容器的首次展示
2、setActionBar()此处进行ActionBar与DrawerLayout通过ActionBarDrawerToggle建立事件关系
3、createMenuList()进行菜单项数据的初始化
4、ViewAnimator类初始化,主要的事件都是通过ViewAnimator来回调主界面中实现的ViewAnimatorListener方法,进行界面

处理(MVP模式体现)
至此,界面初始化完成。

进行操作流程:
1、点击左上方的按钮:
触发ActionBarDrawerToggle的onDrawerSlide方法,根据偏移量slideOffset与linearLayout子菜单项的个数来进行判断,

保证viewAnimator.showMenuContent()只执行一次;显示出侧边完整的菜单
2、viewAnimator.showMenuContent()分析:代码如下(具体可见注释)

public void showMenuContent() {
setViewsClickable(false);
//菜单视图缓存清空
viewList.clear();
double size = list.size();
for (int i = 0; i < size; i++) {
//进行图示绘制
View viewMenu = appCompatActivity.getLayoutInflater().inflate(R.layout.menu_list_item, null);

final int finalI = i;
//子菜单进行点击时间监听
viewMenu.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int[] location = {0, 0};
v.getLocationOnScreen(location);
switchItem(list.get(finalI), location[1] + v.getHeight() / 2);
}
});
((ImageView) viewMenu.findViewById(R.id.menu_item_image)).setImageResource(list.get(i).getImageRes());
viewMenu.setVisibility(View.GONE);
viewMenu.setEnabled(false);
//把子菜单添加入缓存队列
viewList.add(viewMenu);
//回调MainActivity实现的addViewToContainer方法
animatorListener.addViewToContainer(viewMenu);
final double position = i;
//此处是各个子菜单不同延迟delay时间进行动画展示的运算
final double delay = 3 * ANIMATION_DURATION * (position / size);
//通过一个Menu一个线程的方式来延迟进行触发动画播放animateView
//当最后一个Menu展示后,对传入的ContentFragment进行界面的Bitmap绘制,提供给ViewAnimationUtils使用
new Handler().postDelayed(new Runnable() {
public void run() {
if (position < viewList.size()) {
animateView((int) position);
}
if (position == viewList.size() - 1) {
//传入的ContentFragment是实现了screenShotable接口的,进行当前界面的Bitmap绘制
screenShotable.takeScreenShot();
setViewsClickable(true);
}
}
}, (long) delay);
}

}


showMenuContent具体流程:

a、for循环生成对应的菜单项视图,并且缓存;

b、然后通过animatorListener.addViewToContainer回调主界面MainActivity的addViewToContainer方法,添加循环添加视图

到一个LinearLayout控件进行展示;

c、对每个Menu进行点击事件的注册:记录点击的位置到location数组,调用switchItem(之后分解)方法

d、根据Menu的初始化顺序进行delay延迟时间的计算,通过线程延时执行animateView来处理动画展示animateView通过

自定义的动画FlipAnimation来实现动画效果

FlipAnimation分解:代码如下(详细见备注)

public class FlipAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private Camera mCamera;

public FlipAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY) {
//初始角度
mFromDegrees = fromDegrees;
//最后角度
mToDegrees = toDegrees;
//中心坐标
mCenterX = centerX;
mCenterY = centerY;
}

@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
//初始化Camera
mCamera = new Camera();
}

//此处为在加速器时间范围内重复调用,这样来实现动画的流畅播放
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
//根据加速器传入的interpolatedTime时间来计算动画角度变化度数
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);

final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;

final Matrix matrix = t.getMatrix();
//记录当前机位 与restore共同使用
camera.save();
//Y轴翻转
camera.rotateY(degrees);

camera.getMatrix(matrix);
//重置当前机位
camera.restore();
//通过Matrix矩阵来具体实现,设置中心点
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);

}

}

通过重写Animation的applyTransformation方法,并且利用android.graphics.Camera(并非硬件摄像头)与Matrix实现动画Y轴翻转动画

(Camera与Matrix的使用此处不做讲解)

3、点击Menu菜单:调用switchItem方法:代码如下

private void switchItem(Resourceble slideMenuItem, int topPosition) {
this.screenShotable = animatorListener.onSwitch(slideMenuItem, screenShotable, topPosition);
hideMenuContent();
}
回调主界面MainActivity中实现的ViewAnimatorListener.onSwitch接口方法函数,并且返回一个ScreenShotable接口对象
(实际为ContentFragment对象,实现了该接口),最后调用hideMenuContent()方法关闭菜单,hideMenuContent与之前showMenuContent类似

4、回到MainActivity的onSwitch实现方法:见代码

@Override
public ScreenShotable onSwitch(Resourceble slideMenuItem, ScreenShotable screenShotable, int position) {
switch (slideMenuItem.getName()) {
//第一个按钮close返回初始化的contentFragment对象
case ContentFragment.CLOSE:
return screenShotable;
default:
//其他返回一个新的contentFragment对象
return replaceFragment(screenShotable, position);
}
}
除了Close按钮之外
@TargetApi(21)
public static SupportAnimator createCircularReveal(View view, int centerX, int centerY, float startRadius, float endRadius) {
if(LOLLIPOP_PLUS) {
return new SupportAnimatorLollipop(android.view.ViewAnimationUtils.createCircularReveal(view, centerX, centerY, startRadius, endRadius));
} else if(!(view.getParent() instanceof RevealAnimator)) {
throw new IllegalArgumentException("View must be inside RevealFrameLayout or RevealLinearLayout.");
} else {
RevealAnimator revealLayout = (RevealAnimator)view.getParent();
revealLayout.setTarget(view);
revealLayout.setCenter((float)centerX, (float)centerY);
Rect bounds = new Rect();
view.getHitRect(bounds);
ObjectAnimator reveal = ObjectAnimator.ofFloat(revealLayout, "revealRadius", new float[]{startRadius, endRadius});
reveal.addListener(getRevealFinishListener(revealLayout, bounds));
return new SupportAnimatorPreL(reveal);
}
}
static {
LOLLIPOP_PLUS = VERSION.SDK_INT >= 21;
}

执行replaceFragment方法返回一个新的ScreenShotable对象(实际为一个新的contentFragment对象)。

replaceFragment方法分解:代码如下,见备注

public static SupportAnimator createCircularReveal(View view, int centerX, int centerY, float startRadius, float endRadius) {
//当Android版本大于等于5.0的时候调用SupportAnimatorLollipop方法
if(LOLLIPOP_PLUS) {
return new SupportAnimatorLollipop(android.view.ViewAnimationUtils.createCircularReveal(view, centerX, centerY, startRadius, endRadius));
}
//如果Android版本小于5.0的时候进行传入的View的父控件判断
//这里返回activity_main.xml的布局文件,查看传入的View(content_frame) 对象的父控件io.codetail.widget.RevealFrameLayout
,会发现RevealFrameLayout是实现了RevealAnimator的,是不是很巧妙
else if(!(view.getParent() instanceof RevealAnimator)) {
throw new IllegalArgumentException("View must be inside RevealFrameLayout or RevealLinearLayout.");
} else {
//Android小于5.0的时候调用RevealAnimator
RevealAnimator revealLayout = (RevealAnimator)view.getParent();
revealLayout.setTarget(view);
revealLayout.setCenter((float)centerX, (float)centerY);
Rect bounds = new Rect();
view.getHitRect(bounds);
ObjectAnimator reveal = ObjectAnimator.ofFloat(revealLayout, "revealRadius", new float[]{startRadius, endRadius});
reveal.addListener(getRevealFinishListener(revealLayout, bounds));
return new SupportAnimatorPreL(reveal);
}
}
static {
LOLLIPOP_PLUS = VERSION.SDK_INT >= 21;
}
根据备注,会发现这里就能够返回一个SupportAnimator对象到MainActivity进行圆形Reveal动画播放了,并且也实现了界面的切换

总结:大体的执行流程,代码上面已经分析清楚了,有不对的地方欢迎指正。这个开源的思路,代码难度并不大。
重要点在三个地方可以借鉴:
1、采用接口回调的方式来处理,这样很灵活
2、这种菜单实现思路,界面切换的过渡动画可以借鉴
3、Toolbar与自定义菜单相关联的思路
不足之处:
1、定制化的痕迹过于明显,不便于扩展
2、依附于DrawerLayout控件
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: