
上一篇我們自定義了一個(gè)流式布局的
ViewGroup,我們?yōu)榱耸煜ぷ远xViewGroup,就繼續(xù)自定義ViewGroup。這篇的內(nèi)容是是仿照酷狗的側(cè)滑菜單。我們寫(xiě)代碼之前,先想清楚是怎么實(shí)現(xiàn),解析實(shí)現(xiàn)的步驟。實(shí)現(xiàn)側(cè)滑的方式很多種,在這里我選擇繼承
HorizontalScrollView,為什么繼承這個(gè)呢?因?yàn)槔^承這個(gè)的話,我們就不用寫(xiě)childView的move meause layout,這樣就節(jié)約了很大的代碼量和事件,因?yàn)閮?nèi)部HorizontalScrollView已經(jīng)封裝好了。我們?cè)谶@個(gè)控件里面放置兩個(gè)childView,一個(gè)是menu,一個(gè)是content。然后我們處理攔截和快速滑動(dòng)事件就可以了。思路想清楚了我們就開(kāi)始擼碼。首先我們自定義一個(gè)屬性,用于打開(kāi)的時(shí)候
content還有多少可以看到,也就是打開(kāi)的時(shí)候menu距離右邊的距離。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SkiddingMenuLayout">
<attr name="menuRightMargin" format="dimension"/>
</declare-styleable>
</resources>
在初始化的時(shí)候我們通過(guò)menuRightMargin屬性獲取menu真正的寬度
public SkiddingMenuLayout(Context context) {
this(context, null);
}
public SkiddingMenuLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SkiddingMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化自定義屬性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SkiddingMenuLayout);
float rightMargin = array.getDimension(
R.styleable.SkiddingMenuLayout_menuRightMargin, DisplayUtil.dip2px(context, 50));
// 菜單頁(yè)的寬度是 = 屏幕的寬度 - 右邊的一小部分距離(自定義屬性)
mMenuWidth = (int) (DisplayUtil.getScreenWidth(context) - rightMargin);
array.recycle();
}
接著我們?cè)诓季旨虞d完畢的時(shí)候我們指定menu和content的寬度
//xml 布局解析完畢回調(diào)的方法
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//指定寬高
//先拿到整體容器
ViewGroup container = (ViewGroup) getChildAt(0);
int childCount = container.getChildCount();
if (childCount != 2)
throw new RuntimeException("只能放置兩個(gè)子View");
//菜單
mMenuView = container.getChildAt(0);
ViewGroup.LayoutParams meauParams = mMenuView.getLayoutParams();
meauParams.width = mMenuWidth;
//7.0一下的不加這句代碼是正常的 7.0以上的必須加
mMenuView.setLayoutParams(meauParams);
//內(nèi)容頁(yè)
mContentView = container.getChildAt(1);
ViewGroup.LayoutParams contentParams = mContentView.getLayoutParams();
contentParams.width = DisplayUtil.getScreenWidth(getContext());
//7.0一下的不加這句代碼是正常的 7.0以上的必須加
mContentView.setLayoutParams(contentParams);
}
這里有一個(gè)細(xì)節(jié),我們?cè)趧傔M(jìn)入的時(shí)候,菜單默認(rèn)是關(guān)閉的,所以我們需要調(diào)用scrollTo()函數(shù)移動(dòng)一下位置,但是發(fā)現(xiàn)在onFinishInflate()函數(shù)里面調(diào)用沒(méi)有作用,這個(gè)是為什么呢?因?yàn)槲覀冊(cè)?code>xml加載完畢之后,才會(huì)真正的執(zhí)行View的繪制流程,這時(shí)候調(diào)用scrollTo()這個(gè)函數(shù)其實(shí)是執(zhí)行了代碼的,但是在onLaout()擺放childView的時(shí)候,又默認(rèn)回到了(0,0)位置,所以我們應(yīng)該在onLayout()之后調(diào)用這個(gè)函數(shù)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//進(jìn)入是關(guān)閉狀態(tài)
scrollTo(mMenuWidth, 0);
}
初始化完畢了,接下來(lái)我們進(jìn)行事件的攔截,MOVE的時(shí)候相應(yīng)滑動(dòng)事件,UP的時(shí)候判斷是關(guān)閉還是打開(kāi),然后調(diào)用函數(shù)即可
//手指抬起是二選一,要么關(guān)閉要么打開(kāi)
@Override
public boolean onTouchEvent(MotionEvent ev) {
// 當(dāng)菜單打開(kāi)的時(shí)候,手指觸摸右邊內(nèi)容部分需要關(guān)閉菜單,還需要攔截事件(打開(kāi)情況下點(diǎn)擊內(nèi)容頁(yè)不會(huì)響應(yīng)點(diǎn)擊事件)
if (ev.getAction() == MotionEvent.ACTION_UP) {
// 只需要管手指抬起 ,根據(jù)我們當(dāng)前滾動(dòng)的距離來(lái)判斷
int currentScrollX = getScrollX();
if (currentScrollX > mMenuWidth / 2) {
// 關(guān)閉
closeMenu();
} else {
// 打開(kāi)
openMenu();
}
return true;
}
return super.onTouchEvent(ev);
}
/**
* 打開(kāi)菜單 滾動(dòng)到 0 的位置
*/
private void openMenu() {
// smoothScrollTo 有動(dòng)畫(huà)
smoothScrollTo(0, 0);
}
/**
* 關(guān)閉菜單 滾動(dòng)到 mMenuWidth 的位置
*/
private void closeMenu() {
smoothScrollTo(mMenuWidth, 0);
}
到這的話,滑動(dòng)事件和打開(kāi)關(guān)閉事件都完成了,接下來(lái)我們就處理一個(gè)效果的問(wèn)題,這里當(dāng)從左往右滑動(dòng)的時(shí)候,是慢慢打開(kāi)菜單,這時(shí)候content是有一個(gè)慢慢的縮放,menu有一個(gè)放大和透明度變小,而反過(guò)來(lái)關(guān)閉菜單的話就是相反的效果,content慢慢放大,menu縮小和透明度變大。這里還有一個(gè)細(xì)節(jié),就是menu慢慢的退出和進(jìn)入,滑動(dòng)的距離不是和移動(dòng)的距離相同的,所以這里還有一個(gè)平移。接下來(lái)重寫(xiě)onScrollChanged()函數(shù),然后計(jì)算出一個(gè)梯度值來(lái)做處理
//滑動(dòng)改變觸發(fā)
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
// //抽屜效果 兩種一樣
// ViewCompat.setTranslationX(mMenuView, l);
// ViewCompat.setX(mMenuView, l);
// Log.e("zzz", "l->" + l + " t->" + t + " oldl->" + oldl + " oldt->" + oldt);
//主要看l 手指從左往右滑動(dòng) 由大變小
//計(jì)算一個(gè)梯度值 1->0
float scale = 1.0f * l / mMenuWidth;
//酷狗側(cè)滑效果...
// //右邊的縮放 最小是0.7f ,最大是1.0f
float rightScale = 0.7f + 0.3f * scale;
//設(shè)置mContentView縮放的中心點(diǎn)位置
ViewCompat.setPivotX(mContentView, 0);
ViewCompat.setPivotY(mContentView, mContentView.getHeight() / 2);
//設(shè)置右邊縮放
ViewCompat.setScaleX(mContentView, rightScale);
ViewCompat.setScaleY(mContentView, rightScale);
//菜單
//透明度是半透明到全透明 0.5f-1.0f
float alpha = 0.5f + (1.0f - scale) * 0.5f;
ViewCompat.setAlpha(mMenuView, alpha);
//縮放 0.7-1.0
float leftScale = 0.7f + 0.3f * (1 - scale);
ViewCompat.setScaleX(mMenuView, leftScale);
ViewCompat.setScaleY(mMenuView, leftScale);
//退出按鈕在右邊
ViewCompat.setTranslationX(mMenuView, 0.2f * l);
}
這樣的話我們就完成了效果,但是我們還有幾個(gè)細(xì)節(jié)沒(méi)有處理,首先是快速滑動(dòng)的問(wèn)題,還有一個(gè)是當(dāng)打開(kāi)menu的時(shí)候,點(diǎn)擊content需要關(guān)閉菜單,而不是相應(yīng)對(duì)應(yīng)的事件。接下來(lái)我們對(duì)這兩個(gè)問(wèn)題進(jìn)行處理。
快速滑動(dòng)問(wèn)題,這個(gè)問(wèn)題我們采用GestureDetector這個(gè)類來(lái)做處理,這個(gè)類可以處理很多收拾問(wèn)題:
/**
* The listener that is used to notify when gestures occur.
* If you want to listen for all the different gestures then implement
* this interface. If you only want to listen for a subset it might
* be easier to extend {@link SimpleOnGestureListener}.
*/
public interface OnGestureListener {
/**
* Notified when a tap occurs with the down {@link MotionEvent}
* that triggered it. This will be triggered immediately for
* every down event. All other events should be preceded by this.
*
* @param e The down motion event.
*/
boolean onDown(MotionEvent e);
/**
* The user has performed a down {@link MotionEvent} and not performed
* a move or up yet. This event is commonly used to provide visual
* feedback to the user to let them know that their action has been
* recognized i.e. highlight an element.
*
* @param e The down motion event
*/
void onShowPress(MotionEvent e);
/**
* Notified when a tap occurs with the up {@link MotionEvent}
* that triggered it.
*
* @param e The up motion event that completed the first tap
* @return true if the event is consumed, else false
*/
boolean onSingleTapUp(MotionEvent e);
/**
* Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
* current move {@link MotionEvent}. The distance in x and y is also supplied for
* convenience.
*
* @param e1 The first down motion event that started the scrolling.
* @param e2 The move motion event that triggered the current onScroll.
* @param distanceX The distance along the X axis that has been scrolled since the last
* call to onScroll. This is NOT the distance between {@code e1}
* and {@code e2}.
* @param distanceY The distance along the Y axis that has been scrolled since the last
* call to onScroll. This is NOT the distance between {@code e1}
* and {@code e2}.
* @return true if the event is consumed, else false
*/
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
/**
* Notified when a long press occurs with the initial on down {@link MotionEvent}
* that trigged it.
*
* @param e The initial on down motion event that started the longpress.
*/
void onLongPress(MotionEvent e);
/**
* Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
* and the matching up {@link MotionEvent}. The calculated velocity is supplied along
* the x and y axis in pixels per second.
*
* @param e1 The first down motion event that started the fling.
* @param e2 The move motion event that triggered the current onFling.
* @param velocityX The velocity of this fling measured in pixels per second
* along the x axis.
* @param velocityY The velocity of this fling measured in pixels per second
* along the y axis.
* @return true if the event is consumed, else false
*/
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
/**
* The listener that is used to notify when a double-tap or a confirmed
* single-tap occur.
*/
public interface OnDoubleTapListener {
/**
* Notified when a single-tap occurs.
* <p>
* Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
* will only be called after the detector is confident that the user's
* first tap is not followed by a second tap leading to a double-tap
* gesture.
*
* @param e The down motion event of the single-tap.
* @return true if the event is consumed, else false
*/
boolean onSingleTapConfirmed(MotionEvent e);
/**
* Notified when a double-tap occurs.
*
* @param e The down motion event of the first tap of the double-tap.
* @return true if the event is consumed, else false
*/
boolean onDoubleTap(MotionEvent e);
/**
* Notified when an event within a double-tap gesture occurs, including
* the down, move, and up events.
*
* @param e The motion event that occurred during the double-tap gesture.
* @return true if the event is consumed, else false
*/
boolean onDoubleTapEvent(MotionEvent e);
}
/**
* The listener that is used to notify when a context click occurs. When listening for a
* context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
* {@link View#onGenericMotionEvent(MotionEvent)}.
*/
public interface OnContextClickListener {
/**
* Notified when a context click occurs.
*
* @param e The motion event that occurred during the context click.
* @return true if the event is consumed, else false
*/
boolean onContextClick(MotionEvent e);
}
這里我們主要是響應(yīng)onFling()這個(gè)函數(shù),然后判斷當(dāng)前是打開(kāi)還是關(guān)閉狀態(tài),在根據(jù)快速滑動(dòng)的手勢(shì)來(lái)執(zhí)行打開(kāi)還是關(guān)閉的操作:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mGestureDetector.onTouchEvent(ev))//快速滑動(dòng)觸發(fā)了下面的就不要執(zhí)行了
return true;
//....
}
//快速滑動(dòng)
private GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//快速滑動(dòng)回調(diào)
//打開(kāi)的時(shí)候從右到左滑動(dòng)關(guān)閉 關(guān)閉的時(shí)候從左往右打開(kāi)
// Log.e("zzz", "velocityX->" + velocityX);
// >0 從左往右邊滑動(dòng) <0 從右到左
if (mMenuIsOpen) {
if (velocityX < 0) {
closeMenu();
return true;
}
} else {
if (velocityX > 0) {
openMenu();
return true;
}
}
return super.onFling(e1, e2, velocityX, velocityY);
}
};
接下來(lái)處理menu打開(kāi)狀態(tài)下點(diǎn)擊content關(guān)閉menu,這里我們需要用到onInterceptTouchEvent。當(dāng)打開(kāi)狀態(tài)的時(shí)候,我們就把這個(gè)事件攔截,然后關(guān)閉菜單即可。但是這里有一個(gè)問(wèn)題,當(dāng)我們攔截了DOWN事件之后,后面的MOVE UP事件都會(huì)被攔截并且相應(yīng)自身的onTouchEvent事件,所以這里我們需要添加一個(gè)判斷值,判斷是否攔截,然后讓其onTouchEvent是否繼續(xù)執(zhí)行操作
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
isIntercept = false;
if (mMenuIsOpen && ev.getX() > mMenuWidth) {//打開(kāi)狀態(tài) 觸摸右邊關(guān)閉
isIntercept = true;//攔截的話就不執(zhí)行自己的onTouchEvent
closeMenu();
return true;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (isIntercept)//攔截的話就不執(zhí)行自己的onTouchEvent
return true;
//...
}
根據(jù)我們提出需求,然后分析需求,再完成需求。這一步步我們慢慢進(jìn)行滲透,最終完成效果,完成之后你會(huì)發(fā)現(xiàn)其實(shí)也就那么一回事。當(dāng)我們有新需求的時(shí)候,我們應(yīng)該不要恐懼,應(yīng)該欣然樂(lè)觀的接收,再慢慢分析,最終完成。這樣的話我們才能提高我們的技術(shù)。