本篇文章已受權微信公衆號碼個蛋
獨家發佈android
最近又是公司項目上線一段時間了,又是到了程序汪整理代碼的節奏了。恰好也用到了ofo主頁菜單的效果,因而本身把這部分給整理出來,供小夥伴們一塊兒學習學習。仍是和往常同樣,先來個效果圖再說:git
下面進入主題,看看如何搭建這樣的效果,還沒看看本身作出來的效果呢,下面也來看看本身的效果圖吧:github
後加的: canvas
後加的: bash
佈局:微信
<?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:app
//啓動menu
//ofoMenuLayout是最外層的view,用來管理title和content的動畫
ofoMenuLayout.open();
複製代碼
關閉menu:ide
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);
複製代碼
爲了更好地理解代碼,在上代碼以前能夠看看本身畫的圖:
從草圖總體來看,最外層是包裹了OfoMenuLayout,它是專門來管理咱們的title和content部分,不難理解它裏面就兩個直接的孩子:
上面的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網上有對應的圖:
簡單吧,這就是咱們Content部分的背景繪製了,關於Drawable的繪製能夠見: 洪洋大神: blog.csdn.net/lmj62356579…
最後給張咱們Content部分繪製出來的效果圖:
下面就是動態部分的處理了,實際上是對三部分在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();
}
}
複製代碼
歡迎客官到本店光臨:184793647
(qq羣)
email: a1002326270@163.com
csdn:blog.csdn.net/u010429219/…
github:github.com/1002326270x…