蘋果版小黃車(ofo)app主頁菜單效果


代碼地址如下:
http://www.demodashi.com/demo/12823.html

前言:

最近又是公司項目上線一段時間了,又是到了程序汪整理代碼的節奏了。剛好也用到了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群)
蘋果版小黃車(ofo)app主頁菜單效果

代碼地址如下:
http://www.demodashi.com/demo/12823.html

注:本文著作權歸作者,由demo大師代發,拒絕轉載,轉載需要作者授權


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com