[开源学习]SwipeMenuListView源码实现过程解析
2015-12-13 22:16
656 查看
SwipeMenuListView源码实现过程解析
QQ消息窗口一个功能,拖动消息以后在Item中显示出两个按钮,感觉体验不错,虽然这个功能早就有了,但一直没有去思考过实现,于是决定去学习相同功能的开源控件源码-SwipeMenuListView。另外发现网上很多人写了它的使用方法,却没有人解析过它的实现过程,于是希望我文笔拙劣的这一篇,能让阅读之人有所收获。首先我们先看看官方Demo效果:
再看看项目类:
重要的信息类、接口
首先先简单地了解这个开源项目中的几个信息类、和几个接口,把”好戏”留在后头。SwipeMenu:
public class SwipeMenu { private Context mContext; private List<SwipeMenuItem> mItems; private int mViewType; public SwipeMenu(Context context) { mContext = context; mItems = new ArrayList<SwipeMenuItem>(); } public Context getContext() { return mContext; } public void addMenuItem(SwipeMenuItem item) { mItems.add(item); } public void removeMenuItem(SwipeMenuItem item) { mItems.remove(item); } ...//此处省去部分操作SwipeMenuItem方法~ }
SwipeMenuItem:
public class SwipeMenuItem { private int id; private Context mContext; private String title; private Drawable icon; private Drawable background; private int titleColor; private int titleSize; private int width; public SwipeMenuItem(Context context) { mContext = context; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } ...//此处省去很多get和set方法~ }
明显,SwipeMenuItem就是拖动菜单的信息元素,它封装好方法来对拖动菜单子项进行设置的,SwipeMenu是一个装载这些信息容器List,可以对SwipeMenuItem添加和移除等等操作。
SwipeMenuCreator:
public interface SwipeMenuCreator { void create(SwipeMenu menu); }
这个接口用于设置回调SwipeMenu的信息,在SwipeMenuListView中(setMenuCreator)要用这个接口来将拖动菜单的信息传入,然后生成对应的拖动菜单,具体后面分析SwipeMenuListView类的时候会细讲,这里只需要大概知道就好。
另外还有下面这三个接口:
public static interface OnMenuItemClickListener { boolean onMenuItemClick(int position, SwipeMenu menu, int index); } public static interface OnSwipeListener { void onSwipeStart(int position); void onSwipeEnd(int position); } public static interface OnMenuStateChangeListener { void onMenuOpen(int position); void onMenuClose(int position); }
分别用于实现拖动菜单的点击事件、拖动时的坐标更新、拖动菜单状态改变。
另外还设置了系统的动画补间器接口,用于修饰打开和移除时的动画效果:
private Interpolator mCloseInterpolator; private Interpolator mOpenInterpolator;
核心的部分
SwipeMenuView
代码:public class SwipeMenuView extends LinearLayout implements OnClickListener { private SwipeMenuListView mListView; private SwipeMenuLayout mLayout; private SwipeMenu mMenu; private OnSwipeItemClickListener onItemClickListener; private int position; public int getPosition() { return position; } public void setPosition(int position) { this.position = position; } public SwipeMenuView(SwipeMenu menu, SwipeMenuListView listView) { super(menu.getContext()); mListView = listView; mMenu = menu; List<SwipeMenuItem> items = menu.getMenuItems(); int id = 0; for (SwipeMenuItem item : items) { addItem(item, id++); } } private void addItem(SwipeMenuItem item, int id) { LayoutParams params = new LayoutParams(item.getWidth(), LayoutParams.MATCH_PARENT); LinearLayout parent = new LinearLayout(getContext()); parent.setId(id); parent.setGravity(Gravity.CENTER); parent.setOrientation(LinearLayout.VERTICAL); parent.setLayoutParams(params); parent.setBackgroundDrawable(item.getBackground()); parent.setOnClickListener(this); addView(parent); if (item.getIcon() != null) { parent.addView(createIcon(item)); } if (!TextUtils.isEmpty(item.getTitle())) { parent.addView(createTitle(item)); } } private ImageView createIcon(SwipeMenuItem item) { ImageView iv = new ImageView(getContext()); iv.setImageDrawable(item.getIcon()); return iv; } private TextView createTitle(SwipeMenuItem item) { TextView tv = new TextView(getContext()); tv.setText(item.getTitle()); tv.setGravity(Gravity.CENTER); tv.setTextSize(item.getTitleSize()); tv.setTextColor(item.getTitleColor()); return tv; } //此处省略部分方法。 public static interface OnSwipeItemClickListener { void onItemClick(SwipeMenuView view, SwipeMenu menu, int index); } }
这个SwipeMenuView就是侧滑后所看到的菜单,通过它的构造方法将SwipeMenu传进来,for循环遍历List中的元素,通过createIcon、createTitle新建ImageView和TextView对象,实例化一个LinearLayout对象装载ImageView和TextView,将这个LinearLayout添加到自身。点击事件触发时通过接口回调(OnSwipeItemClickListener)将自己及Menu信息传出去。
SwipeMenuLayout:
因为整个SwipeMenuLayout(继承自FrameLayout)的代码稍微多,所以这里分开的挑出几个核心的方法进行解析。其构造方法:
public SwipeMenuLayout(View contentView, SwipeMenuView menuView, Interpolator closeInterpolator, Interpolator openInterpolator) { super(contentView.getContext()); mCloseInterpolator = closeInterpolator; mOpenInterpolator = openInterpolator; mContentView = contentView; mMenuView = menuView; mMenuView.setLayout(this); init(); }
通过构造方法传入SwipeMenuView,及传入动画补间器(加速、减速…),再调用init方法初始化:
private void init() { setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); mGestureListener = new SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { isFling = false; return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // TODO if (Math.abs(e1.getX() - e2.getX()) > MIN_FLING && velocityX < MAX_VELOCITYX) { isFling = true; } return super.onFling(e1, e2, velocityX, velocityY); } }; mGestureDetector = new GestureDetectorCompat(getContext(), mGestureListener); if (mCloseInterpolator != null) { mCloseScroller = ScrollerCompat.create(getContext(), mCloseInterpolator); } else { mCloseScroller = ScrollerCompat.create(getContext()); } if (mOpenInterpolator != null) { mOpenScroller = ScrollerCompat.create(getContext(), mOpenInterpolator); } else { mOpenScroller = ScrollerCompat.create(getContext()); } LayoutParams contentParams = new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); mContentView.setLayoutParams(contentParams); if (mContentView.getId() < 1) { mContentView.setId(CONTENT_VIEW_ID); } mMenuView.setId(MENU_VIEW_ID); mMenuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); addView(mContentView); addView(mMenuView); }
监听手势动作,onFling设置最小滑动动作,设置添加移除的动画效果的补间器Interpolator,及关键在最后添加mContentView及mMenuView,前者是ListView的本身的Item,后者是SwipeMenuView即拖动后显示的菜单。
onSwipe:
public boolean onSwipe(MotionEvent event) { mGestureDetector.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = (int) event.getX(); isFling = false; break; case MotionEvent.ACTION_MOVE: // Log.i("byz", "downX = " + mDownX + ", moveX = " + event.getX()); int dis = (int) (mDownX - event.getX()); if (state == STATE_OPEN) { dis += mMenuView.getWidth()*mSwipeDirection;; } swipe(dis); break; case MotionEvent.ACTION_UP: if ((isFling || Math.abs(mDownX - event.getX()) > (mMenuView.getWidth() / 2)) && Math.signum(mDownX - event.getX()) == mSwipeDirection) { // open smoothOpenMenu(); } else { // close smoothCloseMenu(); return false; } break; } return true; }
里面ACTION_MOVE调用的swipe(dis)方法:
private void swipe(int dis) { if (Math.signum(dis) != mSwipeDirection) { dis = 0; } else if (Math.abs(dis) > mMenuView.getWidth()) { dis = mMenuView.getWidth()*mSwipeDirection; } mContentView.layout(-dis, mContentView.getTop(), mContentView.getWidth() -dis, getMeasuredHeight()); if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) { mMenuView.layout(mContentView.getWidth() - dis, mMenuView.getTop(), mContentView.getWidth() + mMenuView.getWidth() - dis, mMenuView.getBottom()); } else { mMenuView.layout(-mMenuView.getWidth() - dis, mMenuView.getTop(), - dis, mMenuView.getBottom()); } }
里面ACTION_UP时调用的方法smoothCloseMenu、smoothOpenMenu:
public void smoothCloseMenu() { state = STATE_CLOSE; if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) { mBaseX = -mContentView.getLeft(); mCloseScroller.startScroll(0, 0, mMenuView.getWidth(), 0, 350); } else { mBaseX = mMenuView.getRight(); mCloseScroller.startScroll(0, 0, mMenuView.getWidth(), 0, 350); } postInvalidate(); } public void smoothOpenMenu() { state = STATE_OPEN; if (mSwipeDirection == SwipeMenuListView.DIRECTION_LEFT) { mOpenScroller.startScroll(-mContentView.getLeft(), 0, mMenuView.getWidth(), 0, 350); } else { mOpenScroller.startScroll(mContentView.getLeft(), 0, mMenuView.getWidth(), 0, 350); } postInvalidate(); }
这个onSwipe方法是在外部将SwipeMenuListView的触摸事件传入给SwipeMenuLayout时进行调用的。里面的mSwipeDirection是用于设置拖动菜单的出现位置(左边或右边)。将dis(指的是偏移量)传入到swipe方法中,swipe方法中得到偏移量再对mMenuView进行位置的操作,也就是拖动菜单动作。
ACTION_UP中判断坐标偏移量足够大则调用smoothOpenMenu()开启拖动菜单,否则关闭调用smoothCloseMenu()关闭拖动菜单。
在解析SwipeMenuListView之前,我们先解析SwipeMenuAdapter这个类,它是实现这个ListView拖动菜单功能十分关键的部分。
先看看它的核心部分的代码:
public class SwipeMenuAdapter implements WrapperListAdapter, OnSwipeItemClickListener { private ListAdapter mAdapter; private Context mContext; private SwipeMenuListView.OnMenuItemClickListener onMenuItemClickListener; public SwipeMenuAdapter(Context context, ListAdapter adapter) { mAdapter = adapter; mContext = context; } @Override public View getView(int position, View convertView, ViewGroup parent) { SwipeMenuLayout layout = null; if (convertView == null) { View contentView = mAdapter.getView(position, convertView, parent); SwipeMenu menu = new SwipeMenu(mContext); menu.setViewType(getItemViewType(position)); createMenu(menu); SwipeMenuView menuView = new SwipeMenuView(menu, (SwipeMenuListView) parent); menuView.setOnSwipeItemClickListener(this); SwipeMenuListView listView = (SwipeMenuListView) parent; layout = new SwipeMenuLayout(contentView, menuView, listView.getCloseInterpolator(), listView.getOpenInterpolator()); layout.setPosition(position); } else { layout = (SwipeMenuLayout) convertView; layout.closeMenu(); layout.setPosition(position); View view = mAdapter.getView(position, layout.getContentView(), parent); } if (mAdapter instanceof BaseSwipListAdapter) { boolean swipEnable = (((BaseSwipListAdapter) mAdapter).getSwipEnableByPosition(position)); layout.setSwipEnable(swipEnable); } return layout; } public void createMenu(SwipeMenu menu) { // Test Code SwipeMenuItem item = new SwipeMenuItem(mContext); item.setTitle("Item 1"); item.setBackground(new ColorDrawable(Color.GRAY)); item.setWidth(300); menu.addMenuItem(item); item = new SwipeMenuItem(mContext); item.setTitle("Item 2"); item.setBackground(new ColorDrawable(Color.RED)); item.setWidth(300); menu.addMenuItem(item); } //此处省略掉很多实现Adapter方法及一些简单操作方法 }
看看它的构造方法,传进来了一个Adapter,它自身不就是adapter吗,为什么会传进一个Adapter?事实上是SwipeMenuListView在重写setAdapter的地方,将外部传入的Adapter赋给了它,SwipeMenuListView重写的这个我后面再讲。两个Adapter是相互衔接的,或者应该这样说,SwipeMenuAdapter是构造在SwipeMenuListView的Adapter里面的。
我们继续看,SwipeMenuAdapter中的getView方法里面通过以下代码获得一个contentView,可以看到,它的数据来源都是从构造方法传进来的Adapter中get的。
View contentView = mAdapter.getView(position, convertView, parent);
SwipeMenuListView listView = (SwipeMenuListView) parent; layout = new SwipeMenuLayout(contentView, menuView,listView.getCloseInterpolator(),listView.getOpenInterpolator());
然后将得到的这个contentView作为参数去新建一个SwipeMenuLayout,这不就是文章前面解析过的源码吗!传进去的contentView(View)和menuView(SwipeMenuView)就生成了一个新的SwipeMenuLayout并返回!这里耦合的特别好,无论原本ListView的item子项目是构件,都将作为SwipeMenuLayout的一部分,和拖动菜单是分开的,二者完全独立,只有在这个getView方法中才将二者合并。
SwipeMenuListView
终于到这个 最核心的类了,它继承自FrameLayout,首先依然从构造方法开始看起:public SwipeMenuListView(Context context) { super(context); init(); } public SwipeMenuListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } public SwipeMenuListView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { MAX_X = dp2px(MAX_X); MAX_Y = dp2px(MAX_Y); mTouchState = TOUCH_STATE_NONE; }
构造方法中init()设置了一下最大滑动距离,这个距离用于onInterceptTouchEvent判定是否要拦截点击事件,具体我在下面的onInterceptTouchEvent方法解析中一起讲。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { //在拦截处处理,在滑动设置了点击事件的地方也能swip,点击时又不能影响原来的点击事件 int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mDownX = ev.getX(); mDownY = ev.getY(); boolean handled = super.onInterceptTouchEvent(ev); mTouchState = TOUCH_STATE_NONE; mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY()); View view = getChildAt(mTouchPosition - getFirstVisiblePosition()); //只在空的时候赋值 以免每次触摸都赋值,会有多个open状态 if (mTouchView == null && view instanceof SwipeMenuLayout) { mTouchView = (SwipeMenuLayout) view; } //如果摸在 if(mTouchView != null && mTouchView.isOpen() && view != mTouchView){ handled = true; } if (mTouchView != null) { mTouchView.onSwipe(ev); } return handled; case MotionEvent.ACTION_MOVE: float dy = Math.abs((ev.getY() - mDownY)); float dx = Math.abs((ev.getX() - mDownX)); if (Math.abs(dy) > MAX_Y || Math.abs(dx) > MAX_X) { return true; } } return super.onInterceptTouchEvent(ev); }
onInterceptTouchEvent是用于Touch事件的拦截,当返回true的时候,事件会在当前这个ViewGroud被拦截,不继续往下传,并在这一级的View开始消费事件。可以在ACTION_MOVE中看到它时这样操作的:判断当Math.abs(dy) > MAX_Y || Math.abs(dx) > MAX_X,即Y坐标的偏移大雨MAX_Y或X坐标的偏移大于MAX_X时,返回true,拦截事件。
作者真的很细心,这个是为了当我们打开拖动菜单后,能够在拖动菜单的范围内触摸移动来拖动某个Item。因为只要我们移动超过这个偏移的判定值,不把事件传递到Item项中,直接能够拖动Item,佩服作者考虑的如此周全。
接着继续往下看其TouchEvent事件处理,因为代码比较多,我们将Event事件分两部分的case看。
- case:MotionEvent.ACTION_DOWN
@Override public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() != MotionEvent.ACTION_DOWN && mTouchView == null) return super.onTouchEvent(ev); int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: int oldPos = mTouchPosition; mDownX = ev.getX(); mDownY = ev.getY(); mTouchState = TOUCH_STATE_NONE; mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY()); if (mTouchPosition == oldPos && mTouchView != null && mTouchView.isOpen()) { mTouchState = TOUCH_STATE_X; mTouchView.onSwipe(ev); return true; } View view = getChildAt(mTouchPosition - getFirstVisiblePosition()); if (mTouchView != null && mTouchView.isOpen()) { mTouchView.smoothCloseMenu(); mTouchView = null; // return super.onTouchEvent(ev); // try to cancel the touch event MotionEvent cancelEvent = MotionEvent.obtain(ev); cancelEvent.setAction(MotionEvent.ACTION_CANCEL); onTouchEvent(cancelEvent); if (mOnMenuStateChangeListener != null) { mOnMenuStateChangeListener.onMenuClose(oldPos); } return true; } if (view instanceof SwipeMenuLayout) { mTouchView = (SwipeMenuLayout) view; mTouchView.setSwipeDirection(mDirection); } if (mTouchView != null) { mTouchView.onSwipe(ev); } break;
这儿关键的代码在于:
mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
View view = getChildAt(mTouchPosition - getFirstVisiblePosition());
因为ListView是没有ItemOntouch的监听API,所以我们只能自己通过上面的方法来计算当前拖动的item是第几个Item,然后对其进行操作。pointToPosition可以获得点击位置坐标上的Item是第几个,然后再通过getChildAt计算出当前这个item所在的位置(层级),mTouchPosition - getFirstVisiblePosition()就是计算点击的Itemp在当前显示的ListView的Child中的位置(层级)。
case ACTION_MOVE and ACTION_UP:
case MotionEvent.ACTION_MOVE: //有些可能有header,要减去header再判断 mTouchPosition = pointToPosition((int) ev.getX(), (int) ev.getY()) - getHeaderViewsCount(); //如果滑动了一下没完全展现,就收回去,这时候mTouchView已经赋值,再滑动另外一个不可以swip的view //会导致mTouchView swip 。 所以要用位置判断是否滑动的是一个view if (!mTouchView.getSwipEnable() || mTouchPosition != mTouchView.getPosition()) { break; } float dy = Math.abs((ev.getY() - mDownY)); float dx = Math.abs((ev.getX() - mDownX)); if (mTouchState == TOUCH_STATE_X) { if (mTouchView != null) { mTouchView.onSwipe(ev); } getSelector().setState(new int[]{0}); ev.setAction(MotionEvent.ACTION_CANCEL); super.onTouchEvent(ev); return true; } else if (mTouchState == TOUCH_STATE_NONE) { if (Math.abs(dy) > MAX_Y) { mTouchState = TOUCH_STATE_Y; } else if (dx > MAX_X) { mTouchState = TOUCH_STATE_X; if (mOnSwipeListener != null) { mOnSwipeListener.onSwipeStart(mTouchPosition); } } } case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_X) { if (mTouchView != null) { boolean isBeforeOpen = mTouchView.isOpen(); mTouchView.onSwipe(ev); boolean isAfterOpen = mTouchView.isOpen(); if (isBeforeOpen != isAfterOpen && mOnMenuStateChangeListener != null) { if (isAfterOpen) { mOnMenuStateChangeListener.onMenuOpen(mTouchPosition); } else { mOnMenuStateChangeListener.onMenuClose(mTouchPosition); } } if (!isAfterOpen) { mTouchPosition = -1; mTouchView = null; } } if (mOnSwipeListener != null) { mOnSwipeListener.onSwipeEnd(mTouchPosition); } ev.setAction(MotionEvent.ACTION_CANCEL); super.onTouchEvent(ev); return true; } break; } return super.onTouchEvent(ev);
作者自己注释了的那部分我就不说了,在Down的时候已经得到了我们需要操作的那个Item,强制转成SwipeMenuLayout类型(当然还是有instanceof判定),然后在MOVE里面进行坐标上的移动,以及一些操作细节处理,一次只有显示一个拖动菜单。
SwipeMenuListView官方DEMO
看完解析完项目实现,就再简单看看DEMO吧。DEMO中的SimpleActivity,就是上面gif图的效果。
部分定义:
private List<ApplicationInfo> mAppList; private AppAdapter mAdapter; private SwipeMenuListView mListView;
Demo里的Item图片是来自于手机中应用的ICON图标,因而它定义了List mAppList,然后再getView的方法中通过以下得到ICON图标和应用名字:
ApplicationInfo item = getItem(position); holder.iv_icon.setImageDrawable(item.loadIcon(getPackageManager())); holder.tv_name.setText(item.loadLabel(getPackageManager()));
这与源码本身功能无关。接下来我们看AppAdapter的定义及它的父类:
class AppAdapter extends BaseSwipListAdapter { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = View.inflate(getApplicationContext(), R.layout.item_list_app, null); new ViewHolder(convertView); } ViewHolder holder = (ViewHolder) convertView.getTag(); ApplicationInfo item = getItem(position); holder.iv_icon.setImageDrawable(item.loadIcon(getPackageManager())); holder.tv_name.setText(item.loadLabel(getPackageManager())); holder.iv_icon.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(SimpleActivity.this, "iv_icon_click", Toast.LENGTH_SHORT).show(); } }); holder.tv_name.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(SimpleActivity.this,"iv_icon_click",Toast.LENGTH_SHORT).show(); } }); return convertView; } //省略去一堆简单的重写方法、ViewHolder定义即接口。 }
public abstract class BaseSwipListAdapter extends BaseAdapter { public boolean getSwipEnableByPosition(int position){ return true; } }
AppAdapter的定义代码跟我们自己平时写的没有什么区别,实现BaseAdapter中的抽象方法,设置ViewHolder等等。
AppAdapter的父类只定义了一个接口,这个接口用于设定是否设置拖动菜单。
(这个我觉得已经不需要讲了,使用直接去下载源码用用就好了。)
具体实现代码:
mAppList = getPackageManager().getInstalledApplications(0); mListView = (SwipeMenuListView) findViewById(R.id.listView); mAdapter = new AppAdapter(); mListView.setAdapter(mAdapter); // step 1. create a MenuCreator SwipeMenuCreator creator = new SwipeMenuCreator() { @Override public void create(SwipeMenu menu) { // create "open" item SwipeMenuItem openItem = new SwipeMenuItem( getApplicationContext()); // set item background openItem.setBackground(new ColorDrawable(Color.rgb(0xC9, 0xC9, 0xCE))); // set item width openItem.setWidth(dp2px(90)); // set item title openItem.setTitle("Open"); // set item title fontsize openItem.setTitleSize(18); // set item title font color openItem.setTitleColor(Color.WHITE); // add to menu menu.addMenuItem(openItem); // create "delete" item SwipeMenuItem deleteItem = new SwipeMenuItem( getApplicationContext()); // set item background deleteItem.setBackground(new ColorDrawable(Color.rgb(0xF9, 0x3F, 0x25))); // set item width deleteItem.setWidth(dp2px(90)); // set a icon deleteItem.setIcon(R.drawable.ic_delete); // add to menu menu.addMenuItem(deleteItem); } }; // set creator mListView.setMenuCreator(creator); // step 2. listener item click event mListView.setOnMenuItemClickListener(new SwipeMenuListView.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(int position, SwipeMenu menu, int index) { ApplicationInfo item = mAppList.get(position); switch (index) { case 0: open(item); break; case 1: mAppList.remove(position); mAdapter.notifyDataSetChanged(); break; } return false; } });
到此SwipeMenuListView源码解析已完毕,谢谢阅读!
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories