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控件