Side-Menu源码分析讲解

来源:互联网 发布:远程数据采集器 编辑:程序博客网 时间:2024/06/05 09:15

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具体流程:

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

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

到一个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);    }}

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

CameraMatrix的使用此处不做讲解)

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、回到MainActivityonSwitch实现方法:见代码

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