仿苹果版小黄车(ofo)app主页菜单效果

本篇文章已授权微信公众号码个蛋独家发布

前言:

最近又是公司项目上线一段时间了,又是到了程序汪整理代码的节奏了。刚好也用到了ofo主页菜单的效果,于是自己把这部分给整理出来,供小伙伴们一起学习学习。还是和往常一样,先来个效果图再说:

小黄车menu效果.gif

下面进入主题,看看如何搭建这样的效果,还没看看自己做出来的效果呢,下面也来看看自己的效果图吧:

仿制小黄车menu效果.gif

后加的:

添加图片点击事件,切换图片.gif

后加的:

凹进去的menu效果.gif

使用:

布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--模拟的一个启动按钮,这个没什么好说的-->
    <Button
        android:id="@+id/start_ofo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动ofo菜单页面" />

    <!--这个就是我们草图中看到的OfoMenuLayout,
        用来管理title和content两部分的动画以及事件处理-->
    <com.single.ofomenu.view.OfoMenuLayout
        android:id="@+id/ofo_menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:visibility="invisible">

        <!--title部分-->
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="140dp"
            android:background="#fff143">

            <ImageView
                android:id="@+id/close"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_marginRight="10dp"
                android:layout_marginTop="20dp"
                android:src="@drawable/close" />
        </RelativeLayout>

        <!--content部分-->
        <FrameLayout
            android:id="@+id/menu_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="60dp">
            <!--content中列表view,用来处理自己的动画-->
            <com.single.ofomenu.view.OfoContentLayout
                android:id="@+id/ofo_content"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginTop="100dp"
                android:orientation="vertical"
                android:paddingLeft="60dp">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="20dp"
                    android:gravity="center_vertical">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:src="@drawable/folder" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="10dp"
                        android:text="我的资料"
                        android:textSize="16sp" />

                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="20dp"
                    android:gravity="center_vertical">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:src="@drawable/member" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="10dp"
                        android:text="我的会员"
                        android:textSize="16sp" />

                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="20dp"
                    android:gravity="center_vertical">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:src="@drawable/wallet" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="10dp"
                        android:text="我的钱包"
                        android:textSize="16sp" />

                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="20dp"
                    android:gravity="center_vertical">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:src="@drawable/travel" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="10dp"
                        android:text="我的行程"
                        android:textSize="16sp" />

                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="20dp"
                    android:gravity="center_vertical">

                    <ImageView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:src="@drawable/remind" />

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginLeft="10dp"
                        android:text="我的消息"
                        android:textSize="16sp" />

                </LinearLayout>

            </com.single.ofomenu.view.OfoContentLayout>

        </FrameLayout>

    </com.single.ofomenu.view.OfoMenuLayout>

</RelativeLayout>

启动menu:

//启动menu
//ofoMenuLayout是最外层的view,用来管理title和content的动画
ofoMenuLayout.open();

关闭menu:

ofoMenuLayout.close();

menu的监听:

//menu的监听
ofoMenuLayout.setOfoMenuStatusListener(new OfoMenuLayout.OfoMenuStatusListener() {
    @Override
    public void onOpen() {
    }
    @Override
    public void onClose() {
        //to do something,隐藏启动按钮
    }
});

给menu设置content部分:

//给menu设置content部分
ofoMenuLayout.setOfoContentLayout(ofoContentLayout);

讲解:

为了更好地理解代码,在上代码之前可以看看自己画的图:

草图.png

从草图整体来看,最外层是包裹了OfoMenuLayout,它是专门来管理我们的title和content部分,不难理解它里面就两个直接的孩子:

OfoMenuLayout两个直接的孩子布局图.png

上面的title部分就没什么好说的了,就是一个相对布局,右上角放了一个关闭按钮,咱们主要是看下Content部分,静态感受下Content的背景是如何生成的,可以见OfoMenuActivity设置了这么一句代码:

Content背景设置:

FrameLayout menu = (FrameLayout) findViewById(R.id.menu_content);
menu.setBackground(new MenuBrawable(BitmapFactory.decodeResource(getResources(), R.mipmap.bitmap), OfoMenuActivity.this));

可以看到这里new了一个MenuBrawable,没错!!!这里是自定义了一个Drawable,那就去看下MenuBrawable构造器吧:

MenuBrawable构造器:

//外层弧形path
private Path mPath;
//图片对象
private Bitmap bitmap;
private Paint paint;
//绘制图片时要用的画笔,主要为setXfermode做准备
private Paint mBitmapPaint;
//峰值常亮(80dp)
private static final int HEIGHTEST_Y = 80;
//图片宽度(80dp)
private static final int BITMAP_XY = 80;
//弧度的峰值,为后面绘制贝塞尔曲线做准备
private int arcY;
//图片边长
private int bitmapXY;
//图片的中心坐标
private float[] bitmapCneter;
//图片离左边的距离
private int bitmapOffset;
public MenuBrawable(Bitmap bitmap, Context context) {
    this.bitmap = bitmap;
    arcY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, HEIGHTEST_Y, context.getResources().getDisplayMetrics());
    bitmapXY = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, BITMAP_XY, context.getResources().getDisplayMetrics());
    bitmapOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, context.getResources().getDisplayMetrics());
    mPath = new Path();
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.FILL);
}

这里什么也没有干,就初始化了一些常量

下面就是初始化背景path以及图片部分,具体在onBoundsChange方法进行处理:

//bounds对象就是view占据的空间
@Override
protected void onBoundsChange(Rect bounds) {
    super.onBoundsChange(bounds);
    mPath.reset();
    mPath.moveTo(bounds.left, bounds.top + arcY);
    mPath.quadTo(bounds.centerX(), 0, bounds.right, bounds.top + arcY);
    mPath.lineTo(bounds.right, bounds.bottom);
    mPath.lineTo(bounds.left, bounds.bottom);
    mPath.lineTo(bounds.left, bounds.top + arcY);
    if (bitmap != null) {
        mBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //图片的尺寸以小边为主
        int size = Math.min(bitmap.getWidth(), bitmap.getHeight());
        //图片的所放比例
        float scale = (float) (bitmapXY * 1.0 / size);
        Matrix matrix = new Matrix();
        //需要对图片进行缩放
        matrix.setScale(scale, scale);
        //传入上面的matrix裁剪出新的bitmap对象
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
        //生成path的测量工具,主要是获取到path上某一个点,给path上的图片使用
        PathMeasure pathMeasure = new PathMeasure();
        pathMeasure.setPath(mPath, false);
        bitmapCneter = new float[2];
        //通过path的测量工具获取到bitmap的中心位置
        pathMeasure.getPosTan(bitmapOffset, bitmapCneter, null);
    }
}

处理好path轨迹以及bitmap缩放和中心位置确定后,下面就剩下绘制了,Drawable跟我们的View很像,也有自己的绘制。

Drawable绘制:

@Override
public void draw(Canvas canvas) {
    //在初始的图层上绘制path,也就是我们的弧形背景
    canvas.drawPath(mPath, paint);
    //启动一个新的图层
    int layer = canvas.saveLayer(getBounds().left, getBounds().top, getBounds().right, getBounds().bottom, null, Canvas.ALL_SAVE_FLAG);
    //在新的图层上绘制Dst层
    canvas.drawCircle(bitmapCneter[0], bitmapCneter[1], bitmapXY / 2, mBitmapPaint);
    //该mode下取两部分的交集部分
    mBitmapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    //绘制Src层,也就是我们的目标层
    canvas.drawBitmap(bitmap, bitmapCneter[0] - bitmapXY / 2, bitmapCneter[1] - bitmapXY / 2, mBitmapPaint);
    mBitmapPaint.setXfermode(null);
    canvas.restoreToCount(layer);
}

在绘制的时候用到了paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)),关于PorterDuffXfermode传的mode网上有对应的图:

PorterDuffXfermode中mode说明图

简单吧,这就是我们Content部分的背景绘制了,关于Drawable的绘制可以见:
洪洋大神:http://blog.csdn.net/lmj623565791/article/details/43752383/

最后给张我们Content部分绘制出来的效果图:

content部分效果图.png

下面就是动态部分的处理了,其实是对三部分在y轴的平移。下面继续回到我们的草图中,去看下外层的OfoMenuLayout

获取title和content:

private View titleView;
private View contentView;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, old);
    titleView = getChildAt(0);
    contentView = getChildAt(1);
}

菜单打开的动画:

//动画对象
private ObjectAnimator titleAnimator, contentAnimator;

//title起始和终止坐标,主要为动画做准备
private int titleStartY, titleEndY;
//content起始和终止坐标,主要为动画做准备
private int contentStartY, contentEndY;

//菜单打开的动画
public void open() {
    int titleHeight = titleView.getLayoutParams().height;
    //打开菜单的时候title起始坐标正好是y轴负半轴上,也是自己高度的负值
    titleStartY = -titleHeight;
    //打开菜单的时候title终点坐标正好是y轴起点位置
    titleEndY = 0;
    //content起点坐标是在屏幕下面+自身的高度
    contentStartY = getHeight() + contentView.getHeight();
    //终点位置在y轴平移为0
    contentEndY = 0;
    definitAnimation();
    titleAnimator.start();
    contentAnimator.start();
}

定义动画:

//title动画标志,为事件分发做准备
private boolean titleAnimationing;
//content动画标志,为事件分发做准备
private boolean contentAnimationing;

//定义动画部分
private void definitAnimation() {
    PropertyValuesHolder titlePropertyValuesHolder = PropertyValuesHolder.ofFloat("translationY", titleStartY, titleEndY);
    titleAnimator = ObjectAnimator.ofPropertyValuesHolder(titleView, titlePropertyValuesHolder);
    titleAnimator.setDuration(300);

    contentAnimator = ObjectAnimator.ofFloat(contentView, "translationY", contentStartY, contentEndY);
    //这里设置的时间比title要长一点
    contentAnimator.setDuration(500);
    titleAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            super.onAnimationStart(animation);
            titleAnimationing = true;
        }
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            titleAnimationing = false;
        }
    });
    contentAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            super.onAnimationStart(animation);
            contentAnimationing = true;
        }
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            contentAnimationing = false;
            isOpen = !isOpen;
            setVisibility(isOpen ? VISIBLE : INVISIBLE);
            if (isOpen) {
                if (ofoMenuStatusListener != null) {
                    ofoMenuStatusListener.onOpen();
                }
            } else {
                if (ofoMenuStatusListener != null) {
                    ofoMenuStatusListener.onClose();
                }
            }
        }
    });
}

菜单关闭的动画:

//菜单关闭的动画
//content中列表内容布局,它里面也有自己的动画
private OfoContentLayout ofoContentLayout;
public void close() {
    int titleHeight = titleView.getLayoutParams().height;
    titleStartY = 0;
    titleEndY = -titleHeight;
    contentStartY = 0;
    contentEndY = getHeight() + contentView.getHeight();
    definitAnimation();
    titleAnimator.start();
    contentAnimator.start();
    ofoContentLayout.open();
}

上面的打开和关闭的动画,其实就是调换了起始坐标,好了动画就是这么简单啊,需要主要在动画期间是不允许事件分发的,需要处理事件分发部分。
事件处理:

//content中列表内容布局,它里面也有自己的动画
private OfoContentLayout ofoContentLayout;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    return titleAnimationing || contentAnimationing || ofoContentLayout.isAnimationing();
}

两处的动画已经说完了,还就剩下OfoContentLayout中的动画了。下面也来一起看看吧:

初始化所有的child:

//存储每个child的终点坐标
List<Float> endOffset = new ArrayList<>();

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, old);
    for (int i = 0; i < getChildCount(); i++) {
        final View child = getChildAt(i);
        child.setTag(i);
        child.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                child.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                //终点坐标按照每个child的起点坐标+递增15dp
                endOffset.add(child.getTop() + ((int) child.getTag()) *
                        TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getContext().getResources().getDisplayMetrics()));
            }
        });
    }
}

启动OfoContentLayout中动画:

//是否在动画中的标志,为事件分发做准备
private boolean isAnimationing;
//是否添加监听的标志,因为所有的child时间都是一样的,所以监听第一个child就行
private boolean hasListener;

public void open() {
    for (int i = 0; i < getChildCount(); i++) {
        ObjectAnimator oa = ObjectAnimator.ofFloat(getChildAt(i), "translationY", endOffset.get(i), 0);
        oa.setDuration(700);
        if (!hasListener) {
            hasListener = true;
            oa.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
                    super.onAnimationStart(animation);
                    isAnimationing = true;
                }
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    isAnimationing = false;
                    hasListener = false;
                }
            });
        }
        oa.start();
    }
}

总结:

总结图.png

(1)初始化好content和title两部分的位置
(2)自定义好content部分的Drawable(MenuBrawable)
(3)在OfoMenuLayout中处理content和title的打开和关闭动画
(4)在OfoContentLayout中处理打开的动画,它是不需要关闭动画的

欢迎客官到本店光临:184793647(qq群)

关于我:

email: a1002326270@163.com
csdn:http://blog.csdn.net/u010429219/article/details/78042181
github:https://github.com/1002326270xc/OfoMenuView-master

更多你喜欢的文章

仿360手机助手下载按钮
仿苹果版小黄车(ofo)app主页菜单效果
设计一个银行app的最大额度控件
带你实现ViewGroup规定行数、item居中的流式布局
定制一个类似地址选择器的view
3D版翻页公告效果
一分钟搞定触手app主页酷炫滑动切换效果
快速利用RecyclerView的LayoutManager搭建流式布局
用贝塞尔曲线自己写的一个电量显示的控件
快速搞定一个自定义的日历
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350