有关自定义ViewGroup的文章已经很多了,我为什么写这篇文章,对于初学者或者对自定义组件比较生疏的朋友虽然可以拿来主义的用了,但是要一步一步的实现和了解其中的过程和原理才能真真脱离别人的代码,举一反三却不容易,很多博主其实不愿意一步一步的去写,这样很耗时,但是如果能对读者有帮助,能从这篇文章中学会自定义组件就达到我的目的了。
第一步:搭建框架来实现一个3/5和2/5分屏的界面,效果如下:
最外层是一个自定义的ViewGroup布局文件如下:
package com.example.testscrollto;import android.content.Context;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;import android.view.View.MeasureSpec;public class MyScrollView extends ViewGroup{ private int mWidth; private int mHeight; private float mMenuWeight = 3.0f / 5; //菜单界面比例 private View mMenuView; //菜单界面 private View mPriView; //内容界面 public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) { System.out.println("执行了onLayout"); mMenuView.layout(0, 0, (int)(mWidth * mMenuWeight), mHeight); mPriView.layout((int)(mWidth * mMenuWeight), 0, mWidth, mHeight); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { System.out.println("执行了onMeasure"); super.onMeasure(widthMeasureSpec, heightMeasureSpec); /* * onMeasure传入的两个参数是由上一层控件传入的大小,有多种情况,重写该方法时需要对计算控件的实际大小, * 然后调用setMeasuredDimension(int, int)设置实际大小。 * onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。 * 我们需要通过int mode = MeasureSpec.getMode(widthMeasureSpec)得到模式, * 用int size = MeasureSpec.getSize(widthMeasureSpec)得到尺寸。 * mode共有三种情况,取值分别为MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, MeasureSpec.AT_MOST。 * MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。 * MeasureSpec.AT_MOST是最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。 * MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。 */ mWidth = MeasureSpec.getSize(widthMeasureSpec); //获取MyScrollView的宽度 mHeight = MeasureSpec.getSize(heightMeasureSpec); //获取MyScrollView的高度 } /**设置右滑的菜单View*/ public void setMenu(View menu){ mMenuView = menu; addView(mMenuView); } /** * 设置主界面View */ public void setPrimary(View primary){ mPriView = primary; addView(mPriView); }}第二步:按需要设置界面位置
@Override protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) { mMenuView.layout(-(int)(mWidth * mMenuWeight), 0, 0, mHeight); mPriView.layout(0, 0, mWidth, mHeight); }
第三步:实现左右滑动
package com.example.testscrollto;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.widget.Scroller;public class MyScrollView extends ViewGroup{ private Context mContext; private int mWidth; private int mHeight; private float mMenuWeight = 3.0f / 5; //菜单界面比例 private View mMenuView; //菜单界面 private View mPriView; //内容界面 private boolean mIsShowMenu; private Scroller mScroller; public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; mScroller = new Scroller(mContext); } @Override protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) { mMenuView.layout(-(int)(mWidth * mMenuWeight), 0, 0, mHeight); mPriView.layout(0, 0, mWidth, mHeight); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = MeasureSpec.getSize(widthMeasureSpec); //获取MyScrollView的宽度 mHeight = MeasureSpec.getSize(heightMeasureSpec); //获取MyScrollView的高度 } /**设置右滑的菜单View*/ public void setMenu(View menu){ mMenuView = menu; addView(mMenuView); } /** * 设置主界面View */ public void setPrimary(View primary){ mPriView = primary; addView(mPriView); } private float mDownX; @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: System.out.println("ACTION_DOWN"); mDownX = x; //记录按下时的x坐标 break; case MotionEvent.ACTION_UP: System.out.println("ACTION_UP"); int dis = (int) (x - mDownX); //滑动的距离 if(Math.abs(dis) > (mWidth * mMenuWeight / 2)){ if(dis > 0){ //如果>0则是向右滑动 showMenu(); }else{ //如果<0则是向左滑动 hideMenu(); } } break; default: break; } return true; } @Override public void computeScroll() { super.computeScroll(); if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } } public boolean isShowMenu(){ return mIsShowMenu; } public void showMenu(){ if(mIsShowMenu){ return; } mIsShowMenu = true; //标记菜单已经显示 int dx = (int)(mWidth * mMenuWeight); //滑动到目标位置的距离 mScroller.startScroll(getScrollX(), 0, -dx, 0, 500); invalidate(); } public void hideMenu(){ if(!mIsShowMenu){ return; } mIsShowMenu = false; int dx = (int)(mWidth * mMenuWeight); mScroller.startScroll(getScrollX(), 0, dx, 0, 500); invalidate(); } }从上面代码中可以看到下面两句代码触发computeScroll()方法
mScroller.startScroll(getScrollX(), 0, -dx, 0, 500);invalidate();第四步:添加窗口状态切换监听接口
public interface OnMenuChangedListener{ public void onChanged(boolean isShow); } public void setOnMenuChangedListener(OnMenuChangedListener listener){ mListener = listener; }将showMenu()方法和hideMenu()方法修改如下:
public void showMenu(){ if(mIsShowMenu){ return; } mIsShowMenu = true; int dx = (int)(mWidth * mMenuWeight); mScroller.startScroll(getScrollX(), 0, -dx, 0, 500); if(mListener != null){ mListener.onChanged(mIsShowMenu); } invalidate(); } public void hideMenu(){ if(!mIsShowMenu){ return; } mIsShowMenu = false; int dx = (int)(mWidth * mMenuWeight); mScroller.startScroll(getScrollX(), 0, dx, 0, 500); if(mListener != null){ mListener.onChanged(mIsShowMenu); } invalidate(); }在MainActivity中添加监听
mScrollView.setOnMenuChangedListener(new OnMenuChangedListener() { @Override public void onChanged(boolean isShow) { System.out.println("窗口切换了一次"); } });第五步:根据具体业务及需求实现菜单列表及界面(直接贴出代码)
MainActivity.java
package com.example.testrefreshview;import android.app.Activity;import android.graphics.Color;import android.os.Bundle;import android.view.View;import android.view.View.OnClickListener;import android.widget.AdapterView;import android.widget.AdapterView.OnItemClickListener;import android.widget.ArrayAdapter;import android.widget.Button;import android.widget.ListView;import com.example.testrefreshview.RightScrollView.OnMenuChangedListener;/** * 测试具有右滑菜单功能的ViewGroup,RigthScrollView *@author Lqh */public class MainActivity extends Activity { private RightScrollView mRightScrollView; private Button mShowMenuBtn; private ListView mMenuList; private ArrayAdapterRightScrollView.javamAdapter; private String[] menus = {"附近的人", "我的资料", "设置", "游戏", "即时聊天"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.rightscrollview_test); mRightScrollView = (RightScrollView)findViewById(R.id.rightscrollview); final View menu = getLayoutInflater().inflate(R.layout.rightscrollview_menu, null); final View primary = getLayoutInflater().inflate(R.layout.rightscrollview_primary, null); mMenuList = (ListView) menu.findViewById(R.id.list_right_menu); mShowMenuBtn = (Button) primary.findViewById(R.id.btn_showmenu); mAdapter = new ArrayAdapter (this, android.R.layout.simple_list_item_1, menus); mMenuList.setAdapter(mAdapter); mShowMenuBtn.setOnClickListener(new OnClickListener() { public void onClick(View v) { if(mRightScrollView.isShowMenu()){ mRightScrollView.hideMenu(); }else{ mRightScrollView.showMenu(); } } }); mRightScrollView.setOnMenuChangedListener(new OnMenuChangedListener() { public void onChanged(boolean isShow) { if(isShow){ mShowMenuBtn.setText("隐藏菜单"); }else{ mShowMenuBtn.setText("显示菜单"); } } }); mRightScrollView.setMenu(menu); mRightScrollView.setPrimary(primary); mMenuList.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View view, int position, long id) { switch(position){ case 0: primary.setBackgroundColor(Color.CYAN); break; case 1: primary.setBackgroundColor(Color.BLUE); break; case 2: primary.setBackgroundColor(Color.GRAY); break; case 3: primary.setBackgroundColor(Color.MAGENTA); break; case 4: primary.setBackgroundColor(Color.YELLOW); break; } mRightScrollView.hideMenu(); } }); }}
package com.example.testrefreshview;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.widget.Scroller;/** * 具有右滑菜单的ViewGroup,类似于Facebook的主界面 * @author Lqh */public class RightScrollView extends ViewGroup { private Context mContext; private Scroller mScroller; private View mMenuView; private View mPriView; private int mWidth; private int mHeight; private boolean mIsShowMenu; private float mMenuWeight = 3.0f / 5; private OnMenuChangedListener mListener; public RightScrollView(Context context) { this(context, null); } public RightScrollView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; mScroller = new Scroller(mContext); } /**设置右滑的菜单View*/ public void setMenu(View menu){ mMenuView = menu; addView(mMenuView); } /** * 设置主界面View */ public void setPrimary(View primary){ mPriView = primary; addView(mPriView); } public boolean isShowMenu(){ return mIsShowMenu; } public void setOnMenuChangedListener(OnMenuChangedListener listener){ mListener = listener; } public void showMenu(){ if(mIsShowMenu){ return; } mIsShowMenu = true; int dx = (int)(mWidth * mMenuWeight); mScroller.startScroll(getScrollX(), 0, -dx, 0, 500); if(mListener != null){ mListener.onChanged(mIsShowMenu); } invalidate(); } public void hideMenu(){ if(!mIsShowMenu){ return; } mIsShowMenu = false; int dx = (int)(mWidth * mMenuWeight); mScroller.startScroll(getScrollX(), 0, dx, 0, 500); if(mListener != null){ mListener.onChanged(mIsShowMenu); } invalidate(); } private float mDownX; @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: mDownX = x; break; case MotionEvent.ACTION_UP: int dis = (int) (x - mDownX); if(Math.abs(dis) > (mWidth * mMenuWeight / 2)){ if(dis > 0){ showMenu(); }else{ hideMenu(); } } break; } return true; } @Override public void computeScroll() { super.computeScroll(); if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mWidth = MeasureSpec.getSize(widthMeasureSpec); mHeight = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(mWidth, mHeight); int widthSpec = MeasureSpec.makeMeasureSpec((int)(mWidth * mMenuWeight), MeasureSpec.EXACTLY); int heightSpec = MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY); mMenuView.measure(widthSpec, heightSpec); widthSpec = MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY); mPriView.measure(widthSpec, heightSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mMenuView.layout(-(int)(mWidth * mMenuWeight), 0, 0, mHeight); mPriView.layout(0, 0, mWidth, mHeight); } public interface OnMenuChangedListener{ public void onChanged(boolean isShow); } }运行效果: