先来看下面的这张效果图:
上面这张效果图是百度影音的,现在在Android上很流行,最初是由facebook自己实现的,而后各大应用有跟风之势,那么这种侧滑效果是如何实现的呢?
网上现在这种侧滑菜单的例子很对,也有开源的框架sliderMenu,而且可以定义很多样式,但大部分例子,都只是实现了这种类似效果,没有实现一种可移植的框架,仅仅是单页面效果而已,而且集成起来复杂,鉴于此,我自己实现了一套侧滑菜单的框架:
1、最常用的支持左右策划
2、多个页面切换也好不费力,页面切换的逻辑已经实现好了,集成进来,只需要关注自己项目的业务逻辑
3、支持多个页面集成
4、支持退出业务逻辑
先上我自己实现的效果图:
下面 说一下实现原理:
布局文件采用FrameLayout, 在一个FrameLayout下有二个子布局,一个是菜单,另一个是LeftSliderLayout,而LeftSliderLayout下面可以放二个子布局:第一个是阴影布局(左边阴影),第二个是要拖动的内容。,当向右拖动LeftSliderLayout时,就显示露出菜单布局。而向左拖动LeftSliderLayout时,就覆盖菜单布局。
1.FrameLayout的布局文件local_media_fragment.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <include android:id="@+id/main_layout_below" layout="@layout/main_layout_below" /> <com.zhaoxufeng.leftsliderlayout.lib.LeftSliderLayout android:id="@+id/main_slider_layout" android:layout_width="fill_parent" android:layout_height="fill_parent" > <!-- Shadow Child --> <ImageView android:layout_width="15px" android:layout_height="fill_parent" android:contentDescription="@null" android:scaleType="fitXY" android:src="@drawable/main_side_shadow" /> <!-- Main Child --> <include android:id="@+id/main_slider_main" layout="@layout/local_media" /> </com.zhaoxufeng.leftsliderlayout.lib.LeftSliderLayout> </FrameLayout>
上面 xml 中main_layout_below是对应的左边菜单Menu布局文件(这个布局文件是固定的),local_media是你要的拖动布局
2、LeftSliderLayout.java代码
package com.zhaoxufeng.leftsliderlayout.lib; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.Scroller; public class LeftSliderLayout extends ViewGroup { private static final String TAG = "LeftSliderLayout" ; private Scroller mScroller; private VelocityTracker mVelocityTracker; /** * Constant value for touch state * TOUCH_STATE_REST : no touch * TOUCH_STATE_SCROLLING : scrolling */ private static final int TOUCH_STATE_REST = 0; private static final int TOUCH_STATE_SCROLLING = 1; private int mTouchState = TOUCH_STATE_REST; /** * Distance in pixels a touch can wander before we think the user is scrolling */ private int mTouchSlop; /** * Values for saving axis of the last touch event. */ private float mLastMotionX; private float mLastMotionY; /** * Values for VelocityTracker to compute current velocity. * VELOCITY_UNITS in dp * mVelocityUnits in px */ private static final int VELOCITY_UNITS = 1000; private int mVelocityUnits; /** * The minimum velocity for determining the direction. * MINOR_VELOCITY in dp * mMinorVelocity in px */ private static final float MINOR_VELOCITY = 150.0f; private int mMinorVelocity; /** * The width of Sliding distance from left. * And it should be the same with the width of the View below SliderLayout in a FrameLayout. * DOCK_WIDTH in dp * mDockWidth in px */ private static final float SLIDING_WIDTH = 270.0f; private int mSlidingWidth; /** * The default values of shadow. * VELOCITY_UNITS in dp * mVelocityUnits in px */ private static final float DEF_SHADOW_WIDTH = 10.0f; private int mDefShadowWidth; /** * Value for checking a touch event is completed. */ private boolean mIsTouchEventDone = false; /** * Value for checking slider is open. */ private boolean mIsOpen = false; /** * Value for saving the last offset of scroller ’ x-axis. */ private int mSaveScrollX = 0; /** * Value for checking slider is allowed to slide. */ private boolean mEnableSlide = true; private View mMainChild = null; private OnLeftSliderLayoutStateListener mListener = null; /** * Instantiates a new LeftSliderLayout. * * @param context the associated Context * @param attrs AttributeSet */ public LeftSliderLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * Instantiates a new LeftSliderLayout. * * @param context the associated Context * @param attrs AttributeSet * @param defStyle Style */ public LeftSliderLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mScroller = new Scroller(context); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); /** * Convert values in dp to values in px; */ final float fDensity = getResources().getDisplayMetrics().density; mVelocityUnits = (int) (VELOCITY_UNITS * fDensity + 0.5f); mMinorVelocity = (int) (MINOR_VELOCITY * fDensity + 0.5f); mSlidingWidth = (int) (SLIDING_WIDTH * fDensity + 0.5f); mDefShadowWidth = (int) (DEF_SHADOW_WIDTH * fDensity + 0.5f); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // check Measure Mode is Exactly. final int widthMode = MeasureSpec.getMode(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { throw new IllegalStateException("LeftSliderLayout only canmCurScreen run at EXACTLY mode!"); } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY) { throw new IllegalStateException("LeftSliderLayout only can run at EXACTLY mode!"); } // measure child views int nCount = getChildCount(); for (int i = 2; i < nCount; i++) { removeViewAt(i); } nCount = getChildCount(); if (nCount > 0) { if (nCount > 1) { mMainChild = getChildAt(1); getChildAt(0).measure(widthMeasureSpec, heightMeasureSpec); } else { mMainChild = getChildAt(0); } mMainChild.measure(widthMeasureSpec, heightMeasureSpec); } // Set the scrolled position scrollTo(mSaveScrollX, 0); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int nCount = getChildCount(); if (nCount <= 0) { return; } // Set the size and position of Main Child if (mMainChild != null) { mMainChild.layout( l, t, l + mMainChild.getMeasuredWidth(), t + mMainChild.getMeasuredHeight()); } // Set the size and position of Shadow Child if (nCount > 1) { int nLeftChildWidth = 0; View leftChild = getChildAt(0); ViewGroup.LayoutParams layoutParams = leftChild.getLayoutParams(); if (layoutParams.width == ViewGroup.LayoutParams.FILL_PARENT || layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) { nLeftChildWidth = mDefShadowWidth; } else { nLeftChildWidth = layoutParams.width; } leftChild.layout( l - nLeftChildWidth, t, l, t + leftChild.getMeasuredHeight()); } } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { Log.d(TAG,"computeScroll exeuted:" + "x:" + mScroller.getCurrX() + "Y:" + mScroller.getCurrY()) ; scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } } @Override public boolean onTouchEvent(MotionEvent event) { int nCurScrollX = getScrollX(); // check touch point is in the rectangle of Main Child if (mMainChild != null && mTouchState != TOUCH_STATE_SCROLLING && mIsTouchEventDone) { Rect rect = new Rect(); mMainChild.getHitRect(rect); if (!rect.contains((int)event.getX() + nCurScrollX, (int)event.getY())) { return false; } } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); final int action = event.getAction(); final float x = event.getX(); switch (action) { case MotionEvent.ACTION_DOWN: { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } mIsTouchEventDone = false; mLastMotionX = x; break; } case MotionEvent.ACTION_MOVE: { // check slider is allowed to slide. if (!mEnableSlide) { break; } // compute the x-axis offset from last point to current point int deltaX = (int) (mLastMotionX - x); if (nCurScrollX + deltaX < getMinScrollX()) { deltaX = getMinScrollX() - nCurScrollX; mLastMotionX = mLastMotionX - deltaX; } else if (nCurScrollX + deltaX > getMaxScrollX()) { deltaX = getMaxScrollX() - nCurScrollX; mLastMotionX = mLastMotionX - deltaX; } else { mLastMotionX = x; } // Move view to the current point if (deltaX != 0) { scrollBy(deltaX, 0); } // Save the scrolled position mSaveScrollX = getScrollX(); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { // check slider is allowed to slide. if (!mEnableSlide) { break; } final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(mVelocityUnits); // Set open or close state, when get ACTION_UP or ACTION_CANCEL event. if (nCurScrollX < 0) { int velocityX = (int) velocityTracker.getXVelocity(); if (velocityX > mMinorVelocity) { scrollByWithAnim(getMinScrollX() - nCurScrollX); setState(true); } else if (velocityX < -mMinorVelocity) { scrollByWithAnim(-nCurScrollX); setState(false); } else { if (nCurScrollX >= getMinScrollX() / 2) { scrollByWithAnim(- nCurScrollX); setState(false); } else { scrollByWithAnim(getMinScrollX() - nCurScrollX); setState(true); } } } else { if (nCurScrollX > 0) { scrollByWithAnim(-nCurScrollX); } setState(false); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } mTouchState = TOUCH_STATE_REST; mIsTouchEventDone = true; break; } } return true; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if (mListener != null && !mListener.OnLeftSliderLayoutInterceptTouch(ev)) { return false; } if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) { return true; } final float x = ev.getX(); final float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: mLastMotionX = x; mLastMotionY = y; mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; break; case MotionEvent.ACTION_MOVE: final int xDiff = (int) Math.abs(mLastMotionX - x); if (xDiff > mTouchSlop) { if (Math.abs(mLastMotionY - y) / Math.abs(mLastMotionX - x) < 1) mTouchState = TOUCH_STATE_SCROLLING; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mTouchState = TOUCH_STATE_REST; break; } return mTouchState != TOUCH_STATE_REST; } /** * With the horizontal scroll of the animation * * @param nDx x-axis offset */ void scrollByWithAnim(int nDx) { if (nDx == 0) { return; } Log.d(TAG,"scrollByWithAnim:" + "x:" + (getScrollX() + "Y:" + Math.abs(nDx))) ; mScroller.startScroll(getScrollX(), 0, nDx, 0, Math.abs(nDx)); invalidate(); } /** * Get distance of the maximum horizontal scroll * * @return distance in px */ private int getMaxScrollX() { return 0; } /** * Get distance of the minimum horizontal scroll * @return distance in px */ private int getMinScrollX() { return -mSlidingWidth; } /** * Open LeftSlideLayout */ public void open() { Log.d(TAG,"scroll by " + (getMinScrollX() - getScrollX())) ; if (mEnableSlide) { Log.d(TAG,"scroll by " + (getMinScrollX() - getScrollX())) ; scrollByWithAnim(getMinScrollX() - getScrollX()); setState(true); } } /** * Close LeftSlideLayout */ public void close() { if (mEnableSlide) { scrollByWithAnim((-1) * getScrollX()); setState(false); } } /** * Determine whether LeftSlideLayout is open * * @return true-open,false-close */ public boolean isOpen() { return mIsOpen; } /** * Set state of LeftSliderLayout * * @param bIsOpen the new state */ private void setState(boolean bIsOpen) { boolean bStateChanged = false; if (mIsOpen && !bIsOpen) { bStateChanged = true; } else if (!mIsOpen && bIsOpen) { bStateChanged = true; } mIsOpen = bIsOpen; if (bIsOpen) { mSaveScrollX = getMaxScrollX(); } else { mSaveScrollX = 0; } if (bStateChanged && mListener != null) { mListener.OnLeftSliderLayoutStateChanged(bIsOpen); } } /** * enable slide action of LeftSliderLayout * * @param bEnable */ public void enableSlide(boolean bEnable) { mEnableSlide = bEnable; } /** * Set listener to LeftSliderLayout */ public void setOnLeftSliderLayoutListener(OnLeftSliderLayoutStateListener listener) { mListener = listener; } /** * LeftSliderLayout Listener * */ public interface OnLeftSliderLayoutStateListener { /** * Called when LeftSliderLayout’s state has been changed. * * @param bIsOpen the new state */ public void OnLeftSliderLayoutStateChanged(boolean bIsOpen); /** * Called when LeftSliderLayout has got onInterceptTouchEvent. * * @param ev Touch Event * @return true - LeftSliderLayout need to manage the InterceptTouchEvent. * false - LeftSliderLayout don't need to manage the InterceptTouchEvent. */ public boolean OnLeftSliderLayoutInterceptTouch(MotionEvent ev); } }
LeftSliderLayout有一个Listener。它有二个函数,一个是LeftSliderLayout的打开与关闭的状态改变;另一个是InterceptTouchEvent的回调,主要解决的是在拖动内容中有要处理左右滑动的控件与LeftSliderLayout的左右滑动的事件有冲突,当它返回true时,LeftSliderLayout会处理左右滑动,当它返回false时,就不处理左右滑动的事件。
为了实现侧滑菜单框架,故实现了一个BaseActivity,其他Activity只需要继承这个Activity就行,
3、BaseActivity.java代码
package com.zhaoxufeng.leftsliderlayout.example; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.*; import com.zhaoxufeng.leftsliderlayout.R; import com.zhaoxufeng.leftsliderlayout.lib.LeftSliderLayout; import java.util.ArrayList; import java.util.List; /** * 基类Activity,SliderMenu的基础统一框架 * User: zhiwen.nan * Date: 13-10-7 * Time: 下午8:31 * */ public class BaseActivity extends Activity implements LeftSliderLayout.OnLeftSliderLayoutStateListener, View.OnClickListener { private LeftSliderLayout leftSliderLayout; private ImageView mOpenButton; private TextView mTitleText; private ListView mListView; private List<ListItem> mDataList; private long waitTime = 2000; private long touchTime = 0; private static final String TAG = "BaseActivity" ; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override protected void onResume() { super.onResume(); bindView(); initialDataList(); ListViewAdapter listViewAdapter = new ListViewAdapter(BaseActivity.this,mDataList) ; mListView.setAdapter(listViewAdapter); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { finish(); switch (i) { case 0: mTitleText.setText(getText(R.string.categ_local_video_list)); Intent intent = new Intent(BaseActivity.this,LocalMediaActivity.class) ; startActivity(intent); break; case 1: mTitleText.setText(getText(R.string.cate_leida)); Intent radIntent = new Intent(BaseActivity.this,RadoActivity.class) ; startActivity(radIntent); break; case 2: mTitleText.setText(getText(R.string.hot_viedo)); Intent hotIntent = new Intent(BaseActivity.this,HotMediaListActivity.class) ; startActivity(hotIntent); break; case 3: mTitleText.setText(getText(R.string.cate_favrouite_list)); Intent collectIntent = new Intent(BaseActivity.this,CollectListActivity.class) ; startActivity(collectIntent); break; default: leftSliderLayout.close(); break; } } }); } @Override public void onClick(View view) { } @Override public void OnLeftSliderLayoutStateChanged(boolean bIsOpen) { if (bIsOpen) { // Toast.makeText(this, "LeftSliderLayout is open!", Toast.LENGTH_SHORT).show(); Log.d(TAG," leftsilder is open") ; } else { // Toast.makeText(this, "LeftSliderLayout is close!", Toast.LENGTH_SHORT).show(); Log.d(TAG," leftsilder is close") ; } } @Override public boolean OnLeftSliderLayoutInterceptTouch(MotionEvent ev) { return false; } private void initialDataList(){ mDataList = new ArrayList<ListItem>() ; for (int i = 0; i<= 3; i ++) { ListItem listItem = new ListItem(); listItem.setImageType(i); mDataList.add(listItem); } } private void bindView(){ leftSliderLayout = (LeftSliderLayout) findViewById(R.id.main_slider_layout); leftSliderLayout.setOnLeftSliderLayoutListener(this); mOpenButton = (ImageView)findViewById(R.id.openButton) ; mTitleText = (TextView)findViewById(R.id.titleText) ; mListView = (ListView)findViewById(R.id.listTab) ; mOpenButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(leftSliderLayout.isOpen()) { leftSliderLayout.close(); } else { leftSliderLayout.open(); } } }); } public void openLeftSlider(boolean isToOpen){ if(isToOpen) { leftSliderLayout.open(); }else { leftSliderLayout.close(); } } public void enableSlider(boolean isEnable) { if(isEnable) { leftSliderLayout.enableSlide(true); } else { leftSliderLayout.enableSlide(false); } } @Override public void onBackPressed() { if(!leftSliderLayout.isOpen()) { leftSliderLayout.open(); } else { long currentTime = System.currentTimeMillis(); if((currentTime-touchTime)>=waitTime) { Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show(); touchTime = currentTime; }else { finish(); //todo //退出业务逻辑 ,根据项目需求来写 } } } }
关于左侧菜单的业务逻辑都在BaseActivity里处理,另外返回的逻辑也在里面处理,顶部统一的导航栏打开菜单栏业务逻辑,还有左侧菜单跳转的业务逻辑
4、LocalMediaActivity.java
package com.zhaoxufeng.leftsliderlayout.example; import android.app.Activity; import android.database.Cursor; import android.os.Bundle; import android.provider.Contacts; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.widget.*; import com.zhaoxufeng.leftsliderlayout.R; import com.zhaoxufeng.leftsliderlayout.lib.LeftSliderLayout; import com.zhaoxufeng.leftsliderlayout.lib.LeftSliderLayout.OnLeftSliderLayoutStateListener; import java.util.ArrayList; import java.util.List; /** * @author zhiwen.nan * @since 1.0 * 本地视频界面 */ public class LocalMediaActivity extends BaseActivity { private ListView mListView; private TextView mTitleText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.local_media_fragment); mListView = (ListView)findViewById(R.id.localVideoList) ; mTitleText = (TextView)findViewById(R.id.titleText) ; mTitleText.setText("本地视频"); Cursor cursor = getContentResolver().query(Contacts.People.CONTENT_URI, null, null, null, null); startManagingCursor(cursor); ListAdapter listAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_expandable_list_item_1, cursor,new String[]{Contacts.People.NAME},new int[]{android.R.id.text1}); mListView.setAdapter(listAdapter); } @Override public boolean OnLeftSliderLayoutInterceptTouch(MotionEvent ev) { return true; } }
LocalMediaActivity是自己定义的Activty和业务逻辑界面,只需继承BaseActivity即可,其他Activity类似。
以上就是核心代码,源代码下载: