Android自定义控件仿QQ抽屉效果

其实网上类似的实现已经很多了,原理也并不难,只是网上各种demo运行下来,多少都有一些问题。折腾了半天,决定自己实现一个。

首先我们看看实现效果:

对比网上各类demo,这次要实现的主要表现在以下几点:

1.侧滑显示抽屉view
2.侧滑抽屉隐藏view控件点击事件
3.单击任意item隐藏显示的抽屉view
4.滑动list隐藏显示的抽屉view
5.增加SwipeLayout点击事件和Swipe touch事件判断处理
6.优化快速划开多个抽屉隐藏view时多个SwipeLayout滑动状态判断处理,仅显示最后一个滑动的抽屉隐藏view,隐藏前面所有打开的抽屉view(快速滑动时,可能存在多个抽屉view打开情况,网上找的几个demo主要问题都集中在这一块)

实现原理

其实单就一个SwipeLayout的实现原理来讲的话,还是很简单的,实际上单个SwipeLayout隐藏抽屉状态时,应该是这样的:

也就是说,最初的隐藏状态,实际上是将hide view区域layout到conten view的右边,达到隐藏效果,而后显示则是根据拖拽的x值变化来动态的layout 2个view,从而达到一个滑动抽屉效果。
当然,直接重写view的onTouchEvent来动态的layout 2个view是可以实现我们需要的效果的,但是有更好的方法来实现,就是同过ViewDragHelper。
ViewDragHelper是google官方提供的一个专门用于手势分析处理的类,关于ViewDragHelper的基本使用,网上有一大堆的资源。具体的ViewDragHelper介绍以及基本使用方法,本文就不重复造轮子了,此处推荐鸿洋大神的一篇微博:Android ViewDragHelper完全解析 自定义ViewGroup神器。

具体实现

下面我们开始具体的实现。
布局比较简单,这里就不贴代码了,最后会贴上本demo的完整代码地址。

首先我们实现一个继承FrameLayout的自定义SwipeLauout,重写onFinishInflate方法:
这里我们只允许SwipeLayout设置2个子View,ContentLayout是继承LinearLayout的自定义layout,后面会讲到这个,此处先略过;

 @Override
 protected void onFinishInflate() {
  super.onFinishInflate();
  if (getChildCount() != 2) {
   throw new IllegalStateException("Must 2 views in SwipeLayout");
  }
  contentView = getChildAt(0);
  hideView = getChildAt(1);
  if (contentView instanceof ContentLayout)
   ((ContentLayout) contentView).setSwipeLayout(this);
  else {
   throw new IllegalStateException("content view must be an instanceof FrontLayout");
  }
 }

接着重写onSizeChanged,onLayout,onInterceptTouchEvent方法:

 @Override
 protected void onSizeChanged(int w,int h,int oldw,int oldh) {
  super.onSizeChanged(w,h,oldw,oldh);
  hideViewHeight = hideView.getMeasuredHeight();
  hideViewWidth = hideView.getMeasuredWidth();
  contentWidth = contentView.getMeasuredWidth();
 }

 @Override
 protected void onLayout(boolean changed,int left,int top,int right,int bottom) {
  // super.onLayout(changed,left,top,right,bottom);
  contentView.layout(0,contentWidth,hideViewHeight);
  hideView.layout(contentView.getRight(),contentView.getRight()
    + hideViewWidth,hideViewHeight);
 }

 @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
  boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
  //  Log.e("SwipeLayout","-----onInterceptTouchEvent-----");
  return result;
 }

然后是比较关键的,重写onTouchEvent方法以及ViewDragHelper.Callback回调,我们定了一个enum来判断SwipeLayout的三种状态。在onViewPositionChanged中,有2种方法实现content view和hide view的伴随移动,一种是直接offset view的横向变化量,还有一种就是直接通过layout的方式,两种方式都可以。

 public enum SwipeState {
  Open,Swiping,Close;
 }
 @Override
 public boolean onTouchEvent(MotionEvent event) {
  //  Log.e("SwipeLayout","-----onTouchEvent-----");
  switch (event.getAction()) {
   case MotionEvent.ACTION_DOWN:
    downX = event.getX();
    downY = event.getY();
    break;
   case MotionEvent.ACTION_MOVE:
    // 1.获取x和y方向移动的距离
    float moveX = event.getX();
    float moveY = event.getY();
    float delatX = moveX - downX;// x方向移动的距离
    float delatY = moveY - downY;// y方向移动的距离

    if (Math.abs(delatX) > Math.abs(delatY)) {
     // 表示移动是偏向于水平方向,那么应该SwipeLayout应该处理,请求listview不要拦截
     this.requestDisallowInterceptTouchEvent(true);
    }

    // 更新downX,downY
    downX = moveX;
    downY = moveY;
    break;
   case MotionEvent.ACTION_UP:

    break;
  }
  viewDragHelper.processTouchEvent(event);
  return true;
 }

 private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
  @Override
  public boolean tryCaptureView(View child,int pointerId) {
   return child == contentView || child == hideView;
  }

  @Override
  public int getViewHorizontalDragRange(View child) {
   return hideViewWidth;
  }

  @Override
  public int clampViewPositionHorizontal(View child,int dx) {
   if (child == contentView) {
    if (left > 0)
     left = 0;
    if (left < -hideViewWidth)
     left = -hideViewWidth;
   } else if (child == hideView) {
    if (left > contentWidth)
     left = contentWidth;
    if (left < (contentWidth - hideViewWidth))
     left = contentWidth - hideViewWidth;
   }
   return left;
  }

  @Override
  public void onViewPositionChanged(View changedView,int dx,int dy) {
   super.onViewPositionChanged(changedView,dx,dy);
   if (changedView == contentView) {
    // 如果手指滑动deleteView,那么也要讲横向变化量dx设置给contentView
    hideView.offsetLeftAndRight(dx);
   } else if (changedView == hideView) {
    // 如果手指滑动contentView,那么也要讲横向变化量dx设置给deleteView
    contentView.offsetLeftAndRight(dx);
   }

   //   if (changedView == contentView) {
   //    // 手动移动deleteView
   //    hideView.layout(hideView.getLeft() + dx,//      hideView.getTop() + dy,hideView.getRight() + dx,//      hideView.getBottom() + dy);
   //   } else if (hideView == changedView) {
   //    // 手动移动contentView
   //    contentView.layout(contentView.getLeft() + dx,//      contentView.getTop() + dy,contentView.getRight() + dx,//      contentView.getBottom() + dy);
   //   }
   //实时更新当前状态
   updateSwipeStates();
   invalidate();
  }

  @Override
  public void onViewReleased(View releasedChild,float xvel,float yvel) {
   super.onViewReleased(releasedChild,xvel,yvel);
   //根据用户滑动速度处理开关
   //xvel: x方向滑动速度
   //yvel: y方向滑动速度
   //   Log.e("tag","currentState = " + currentState);
   //   Log.e("tag","xvel = " + xvel);
   if (xvel < -200 && currentState != SwipeState.Open) {
    open();
    return;
   } else if (xvel > 200 && currentState != SwipeState.Close) {
    close();
    return;
   }

   if (contentView.getLeft() < -hideViewWidth / 2) {
    // 打开
    open();
   } else {
    // 关闭
    close();
   }
  }
 };

open(),close()实现

 public void open() {
  open(true);
 }

 public void close() {
  close(true);
 }

 /**
  * 打开的方法
  *
  * @param isSmooth 是否通过缓冲动画的形式设定view的位置
  */
 public void open(boolean isSmooth) {
  if (isSmooth) {
   viewDragHelper.smoothSlideViewTo(contentView,-hideViewWidth,contentView.getTop());
   ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
  } else {
   contentView.offsetLeftAndRight(-hideViewWidth);//直接偏移View的位置
   hideView.offsetLeftAndRight(-hideViewWidth);//直接偏移View的位置
   //   contentView.layout(-hideViewWidth,contentWidth - hideViewWidth,hideViewHeight);//直接通过坐标摆放
   //   hideView.layout(contentView.getRight(),hideViewWidth,hideViewHeight);//直接通过坐标摆放
   invalidate();
  }
 }

 /**
  * 关闭的方法
  *
  * @param isSmooth true:通过缓冲动画的形式设定view的位置
  *     false:直接设定view的位置
  */
 public void close(boolean isSmooth) {
  if (isSmooth) {
   viewDragHelper.smoothSlideViewTo(contentView,contentView.getTop());
   ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
  } else {
   contentView.offsetLeftAndRight(hideViewWidth);
   hideView.offsetLeftAndRight(hideViewWidth);
   invalidate();
   //contentView.layout(0,hideViewHeight);//直接通过坐标摆放
   //hideView.layout(contentView.getRight(),hideViewHeight);//直接通过坐标摆放
  }
 }

此上基本实现了单个SwipeLayout的抽屉滑动效果,但是将此SwipeLayout作为一个item布局设置给一个listView的时候,还需要做许多的判断。

由于listView的重用机制,我们这里并未针对listview做任何处理,所以一旦有一个item的SwipeLayout的状态是打开状态,不可避免的其它也必然有几个是打开状态,所以我们这里需要根据检测listView的滑动,当listView滑动时,关闭SwipeLayout。既然需要在外部控制SwipeLayout的开关,我们先定义一个SwipeLayoutManager用于管理SwipeLayout的控制。

public class SwipeLayoutManager {
 //记录打开的SwipeLayout集合
 private HashSet<SwipeLayout> mUnClosedSwipeLayouts = new HashSet<SwipeLayout>();

 private SwipeLayoutManager() {
 }

 private static SwipeLayoutManager mInstance = new SwipeLayoutManager();

 public static SwipeLayoutManager getInstance() {
  return mInstance;
 }

 /**
  * 将一个没有关闭的SwipeLayout加入集合
  * @param layout
  */
 public void add(SwipeLayout layout) {
  mUnClosedSwipeLayouts.add(layout);
 }

 /**
  * 将一个没有关闭的SwipeLayout移出集合
  * @param layout
  */
 public void remove(SwipeLayout layout){
  mUnClosedSwipeLayouts.remove(layout);
 }

 /**
  * 关闭已经打开的SwipeLayout
  */
 public void closeUnCloseSwipeLayout() {
  if(mUnClosedSwipeLayouts.size() == 0){
   return;
  }

  for(SwipeLayout l : mUnClosedSwipeLayouts){
   l.close(true);
  }
  mUnClosedSwipeLayouts.clear();
 }

 /**
  * 关闭已经打开的SwipeLayout
  */
 public void closeUnCloseSwipeLayout(boolean isSmooth) {
  if(mUnClosedSwipeLayouts.size() == 0){
   return;
  }

  for(SwipeLayout l : mUnClosedSwipeLayouts){
   l.close(isSmooth);
  }
  mUnClosedSwipeLayouts.clear();
 }
}

这样就可以监听listView的滑动,然后在listView滑动的时候,关闭所有的抽屉View。

 listView.setOnScrollListener(new OnScrollListener() {
   @Override
   public void onScrollStateChanged(AbsListView view,int scrollState) {
    swipeLayoutManager.closeUnCloseSwipeLayout();
   }

   @Override
   public void onScroll(AbsListView view,int firstVisibleItem,int visibleItemCount,int totalItemCount) {
   }
  });

考虑到大多数时候,在我们打开抽屉View和关闭抽屉View的时候,外部需要知道SwipeLayout的状态值,所以我们需要在SwipeLayout中增加几个接口,告诉外部当前SwipeLayout的状态值:

SwipeLayout.java

----------------
 private void updateSwipeStates() {
  SwipeState lastSwipeState = currentState;
  SwipeState swipeState = getCurrentState();

  if (listener == null) {
   try {
    throw new Exception("please setOnSwipeStateChangeListener first!");
   } catch (Exception e) {
    e.printStackTrace();
   }
   return;
  }

  if (swipeState != currentState) {
   currentState = swipeState;
   if (currentState == SwipeState.Open) {
    listener.onOpen(this);
    // 当前的Swipelayout已经打开,需要让Manager记录
    swipeLayoutManager.add(this);
   } else if (currentState == SwipeState.Close) {
    listener.onClose(this);
    // 说明当前的SwipeLayout已经关闭,需要让Manager移除
    swipeLayoutManager.remove(this);
   } else if (currentState == SwipeState.Swiping) {
    if (lastSwipeState == SwipeState.Open) {
     listener.onStartClose(this);
    } else if (lastSwipeState == SwipeState.Close) {
     listener.onStartOpen(this);
     //hideView准备显示之前,先将之前打开的的SwipeLayout全部关闭
     swipeLayoutManager.closeUnCloseSwipeLayout();
     swipeLayoutManager.add(this);
    }
   }
  } else {
   currentState = swipeState;
  }
 }

 /**
  * 获取当前控件状态
  *
  * @return
  */
 public SwipeState getCurrentState() {
  int left = contentView.getLeft();
  //  Log.e("tag","contentView.getLeft() = " + left);
  //  Log.e("tag","hideViewWidth = " + hideViewWidth);
  if (left == 0) {
   return SwipeState.Close;
  }

  if (left == -hideViewWidth) {
   return SwipeState.Open;
  }
  return SwipeState.Swiping;
 }
 private OnSwipeStateChangeListener listener;

 public void setOnSwipeStateChangeListener(
   OnSwipeStateChangeListener listener) {
  this.listener = listener;
 }

 public View getContentView() {
  return contentView;
 }

 public interface OnSwipeStateChangeListener {
  void onOpen(SwipeLayout swipeLayout);

  void onClose(SwipeLayout swipeLayout);

  void onStartOpen(SwipeLayout swipeLayout);

  void onStartClose(SwipeLayout swipeLayout);
 }

然后接下来是写一个为listView设置的SwipeAdapter

SwipeAdapter.java

------------
public class SwipeAdapter extends BaseAdapter implements OnSwipeStateChangeListener {
 private Context mContext;
 private List<String> list;
 private MyClickListener myClickListener;
 private SwipeLayoutManager swipeLayoutManager;

 public SwipeAdapter(Context mContext) {
  super();
  this.mContext = mContext;
  init();
 }

 private void init() {
  myClickListener = new MyClickListener();
  swipeLayoutManager = SwipeLayoutManager.getInstance();
 }

 public void setList(List<String> list){
  this.list = list;
  notifyDataSetChanged();
 }

 @Override
 public int getCount() {
  return list.size();
 }

 @Override
 public Object getItem(int position) {
  return list.get(position);
 }

 @Override
 public long getItemId(int position) {
  return position;
 }

 @Override
 public View getView(final int position,View convertView,ViewGroup parent) {
  if (convertView == null) {
   convertView = UIUtils.inflate(R.layout.list_item_swipe);
  }
  ViewHolder holder = ViewHolder.getHolder(convertView);

  holder.tv_content.setText(list.get(position));
  holder.tv_overhead.setOnClickListener(myClickListener);
  holder.tv_overhead.setTag(position);
  holder.tv_delete.setOnClickListener(myClickListener);
  holder.tv_delete.setTag(position);
  holder.sv_layout.setOnSwipeStateChangeListener(this);
  holder.sv_layout.setTag(position);

  holder.sv_layout.getContentView().setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    ToastUtils.showToast("item click : " + position);
    swipeLayoutManager.closeUnCloseSwipeLayout();
   }
  });

  return convertView;
 }

 static class ViewHolder {
  TextView tv_content,tv_overhead,tv_delete;
  SwipeLayout sv_layout;

  public ViewHolder(View convertView) {
   tv_content = (TextView) convertView.findViewById(R.id.tv_content);
   tv_overhead = (TextView) convertView.findViewById(R.id.tv_overhead);
   tv_delete = (TextView) convertView.findViewById(R.id.tv_delete);
   sv_layout = (SwipeLayout) convertView.findViewById(R.id.sv_layout);
  }

  public static ViewHolder getHolder(View convertView) {
   ViewHolder holder = (ViewHolder) convertView.getTag();
   if (holder == null) {
    holder = new ViewHolder(convertView);
    convertView.setTag(holder);
   }
   return holder;
  }
 }

 class MyClickListener implements View.OnClickListener {
  @Override
  public void onClick(View v) {
   Integer position = (Integer) v.getTag();
   switch (v.getId()) {
    case R.id.tv_overhead:
     //ToastUtils.showToast("position : " + position + " overhead is clicked.");
     }
     break;
    case R.id.tv_delete:
     //ToastUtils.showToast("position : " + position + " delete is clicked.");
     }
     break;
    default:
     break;
   }
  }
 }

 @Override
 public void onOpen(SwipeLayout swipeLayout) {
  //ToastUtils.showToast(swipeLayout.getTag() + "onOpen.");
 }

 @Override
 public void onClose(SwipeLayout swipeLayout) {
  //ToastUtils.showToast(swipeLayout.getTag() + "onClose.");
 }

 @Override
 public void onStartOpen(SwipeLayout swipeLayout) {
  //   ToastUtils.showToast("onStartOpen.");
 }

 @Override
 public void onStartClose(SwipeLayout swipeLayout) {
  //   ToastUtils.showToast("onStartClose.");
 }
}

此时已经基本实现了我们需要的大部分功能了,但是当我们滑动的时候,又发现新的问题,我们的SwipeLayout和listview滑动判断有问题。由于前面我们仅仅是将touch拦截事件简简单单的丢给了viewDragHelper.shouldInterceptTouchEvent(ev)来处理,导致SwipeLayout和listview拦截touch事件时的处理存在一定的问题,这里我们要提到一个知识点:Android view事件的传递。
(1)首先由Activity分发,分发给根View,也就是DecorView(DecorView为整个Window界面的最顶层View)
(2)然后由根View分发到子的View

view事件拦截如下图所示:

view事件的消费如下图所示:

注:以上2张图借鉴网上总结的比较经典的图

所以这里我们就要谈到一开始出现的ContentLayout,主要重写了onInterceptTouchEvent和onTouchEvent。

public class ContentLayout extends LinearLayout {
 SwipeLayoutInterface mISwipeLayout;

 public ContentLayout(Context context) {
  super(context);
 }

 public ContentLayout(Context context,AttributeSet attrs) {
  super(context,attrs);
 }

 public ContentLayout(Context context,AttributeSet attrs,int defStyleAttr) {
  super(context,attrs,defStyleAttr);
 }

 public void setSwipeLayout(SwipeLayoutInterface iSwipeLayout) {
  this.mISwipeLayout = iSwipeLayout;
 }

 @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
//  Log.e("ContentLayout","-----onInterceptTouchEvent-----");
  if (mISwipeLayout.getCurrentState() == SwipeState.Close) {
   return super.onInterceptTouchEvent(ev);
  } else {
   return true;
  }
 }

 @Override
 public boolean onTouchEvent(MotionEvent ev) {
//  Log.e("ContentLayout","-----onTouchEvent-----");
  if (mISwipeLayout.getCurrentState() == SwipeState.Close) {
   return super.onTouchEvent(ev);
  } else {
   if (ev.getActionMasked() == MotionEvent.ACTION_UP) {
    mISwipeLayout.close();
   }
   return true;
  }
 }
}

另外由于在ContentLayout中需要拿到父View SwipeLayout的开关状态以及控制SwipeLayout的关闭,因此在再写一个接口,用于ContentLayout获取SwipeLayout的开关状态以及更新SwipeLayout。

public interface SwipeLayoutInterface {

 SwipeState getCurrentState();

 void open();

 void close();
}

然后接着的是完善SwipeLayout的onInterceptTouchEvent,我们在这里增加一个GestureDetectorCompat处理手势识别:

 private void init(Context context) {
  viewDragHelper = ViewDragHelper.create(this,callback);
  mGestureDetector = new GestureDetectorCompat(context,mOnGestureListener);
  swipeLayoutManager = SwipeLayoutManager.getInstance();
 }

 private SimpleOnGestureListener mOnGestureListener = new SimpleOnGestureListener() {
  @Override
  public boolean onScroll(MotionEvent e1,MotionEvent e2,float distanceX,float distanceY) {
   // 当横向移动距离大于等于纵向时,返回true
   return Math.abs(distanceX) >= Math.abs(distanceY);
  }
 };
  @Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {
  boolean result = viewDragHelper.shouldInterceptTouchEvent(ev) & mGestureDetector.onTouchEvent(ev);
  //  Log.e("SwipeLayout","-----onInterceptTouchEvent-----");
  return result;
 }

如此下来,整个View不管是上下拖动,还是SwipeLayout的开关滑动,都已经实现完成了。最后增加对应overhead,delete以及item的点击事件,此处完善SwipeAdapter的代码之后如下。

 class MyClickListener implements View.OnClickListener {
  @Override
  public void onClick(View v) {
   Integer position = (Integer) v.getTag();
   switch (v.getId()) {
    case R.id.tv_overhead:
     //ToastUtils.showToast("position : " + position + " overhead is clicked.");
     swipeLayoutManager.closeUnCloseSwipeLayout(false);
     if(onSwipeControlListener != null){
      onSwipeControlListener.onOverhead(position,list.get(position));
     }
     break;
    case R.id.tv_delete:
     //ToastUtils.showToast("position : " + position + " delete is clicked.");
     swipeLayoutManager.closeUnCloseSwipeLayout(false);
     if(onSwipeControlListener != null){
      onSwipeControlListener.onDelete(position,list.get(position));
     }
     break;
    default:
     break;
   }
  }
 }

 private OnSwipeControlListener onSwipeControlListener;

 public void setOnSwipeControlListener(OnSwipeControlListener onSwipeControlListener){
  this.onSwipeControlListener = onSwipeControlListener;
 }

 /**
  * overhead 和 delete点击事件接口
  */
 public interface OnSwipeControlListener{
  void onOverhead(int position,String itemTitle);

  void onDelete(int position,String itemTitle);
 }

最后贴上MainActivity代码,此处通过OnSwipeControlListener接口回调实现item的删除和置顶:

public class MainActivity extends Activity implements OnSwipeControlListener {
 private ListView listView;
 private List<String> list = new ArrayList<String>();
 private SwipeLayoutManager swipeLayoutManager;
 private SwipeAdapter swipeAdapter;

 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  initData();
  initView();
 }

 private void initData() {
  for (int i = 0; i < 50; i++) {
   list.add("content - " + i);
  }
 }

 private void initView() {
  swipeLayoutManager = SwipeLayoutManager.getInstance();
  swipeAdapter = new SwipeAdapter(this);
  swipeAdapter.setList(list);

  listView = (ListView) findViewById(R.id.list_view);

  listView.setAdapter(swipeAdapter);
  listView.setOnScrollListener(new OnScrollListener() {
   @Override
   public void onScrollStateChanged(AbsListView view,int totalItemCount) {
   }
  });

  swipeAdapter.setOnSwipeControlListener(this);
 }

 @Override
 public void onOverhead(int position,String itemTitle) {
  setItemOverhead(position,itemTitle);
 }

 @Override
 public void onDelete(int position,String itemTitle) {
  removeItem(position,itemTitle);
 }

 /**
  * 设置item置顶
  *
  * @param position
  * @param itemTitle
  */
 private void setItemOverhead(int position,String itemTitle) {
  // ToastUtils.showToast("position : " + position + " overhead.");
  ToastUtils.showToast("overhead ---" + itemTitle + "--- success.");
  String newTitle = itemTitle;
  list.remove(position);//删除要置顶的item
  list.add(0,newTitle);//根据adapter传来的Title数据在list 0位置插入title字符串,达到置顶效果
  swipeAdapter.setList(list);//重新给Adapter设置list数据并更新
  UIUtils.runOnUIThread(new Runnable() {
   @Override
   public void run() {
    listView.setSelection(0);//listview选中第0项item
   }
  });
 }

 /**
  * 删除item
  *
  * @param position
  * @param itemTitle
  */

 private void removeItem(int position,String itemTitle) {
  //  ToastUtils.showToast("position : " + position + " delete.");
  ToastUtils.showToast("delete ---" + itemTitle + "--- success.");
  list.remove(position);
  swipeAdapter.setList(list);//重新给Adapter设置list数据并更新
 }
}

至此整个demo基本完成,本次完成的功能基本能够直接放到项目中使用。其实最麻烦的地方就在于view的touch事件拦截和处理,不过将本demo的log打开看一下对比之后,也就能够理解整个传递过程了。

完整demo地址:https://github.com/Horrarndoo/SwipeLayout

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

以上是来客网为你收集整理的Android自定义控件仿QQ抽屉效果全部内容,希望文章能够帮你解决Android自定义控件仿QQ抽屉效果所遇到的程序开发问题。

如果觉得来客网网站内容还不错,欢迎将来客网网站推荐给程序员好友。