Side-Menu源码分析讲解

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值