DrawerLayout 源码分析
2016-07-17 23:31
429 查看
简介
DrawerLayout充当窗口内容的顶层容器,允许”抽屉”式的控件可以从窗口的一边或者两边垂直边缘拉出
使用
抽屉的位置或者布局可以通过android:layout_gravity子view的属性控制从那边拉出,left/start代表从左边拉出,right/end代表从右侧拉出,需要注意的是只能有一个抽屉控件从窗口的垂直边缘,如果布局中每个垂直窗口有多于一个抽屉控件,将会抛出异常
根布局使用
DrawerLayout作为第一个主内容布局,主内容布局宽高设置为
match_parent不用设置
layout_gravity,然后在主内容布局上添加子控件,并且设置
layout_gravity
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawerlayout" android:layout_width="match_parent" android:layout_height="match_parent" > <FrameLayout android:id="@+id/fragment_layout" android:layout_width="match_parent" android:layout_height="match_parent" > </FrameLayout> <RelativeLayout android:id="@+id/left" android:layout_width="200dp" android:layout_height="match_parent" android:layout_gravity="left" android:background="@android:color/white"> <ListView android:id="@+id/left_listview" android:layout_width="match_parent" android:layout_height="match_parent" > </ListView> </RelativeLayout> <RelativeLayout android:id="@+id/right" android:layout_width="260dp" android:layout_height="match_parent" android:layout_gravity="right" android:background="@android:color/holo_green_light"> <TextView android:id="@+id/right_textview" android:layout_width="match_parent" android:layout_height="match_parent" android:text="个人登陆页面" /> </RelativeLayout> </android.support.v4.widget.DrawerLayout>
详细代码请参考 http://blog.csdn.net/elinavampire/article/details/41477525
源码分析
构造函数
public DrawerLayout(Context context) { this(context, null); } public DrawerLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); final float density = getResources().getDisplayMetrics().density; mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f); final float minVel = MIN_FLING_VELOCITY * density; mLeftCallback = new ViewDragCallback(Gravity.LEFT); mRightCallback = new ViewDragCallback(Gravity.RIGHT); mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback); mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); mLeftDragger.setMinVelocity(minVel); mLeftCallback.setDragger(mLeftDragger); mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback); mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); mRightDragger.setMinVelocity(minVel); mRightCallback.setDragger(mRightDragger); // So that we can catch the back button setFocusableInTouchMode(true); ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); ViewGroupCompat.setMotionEventSplittingEnabled(this, false); if (ViewCompat.getFitsSystemWindows(this)) { IMPL.configureApplyInsets(this); mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context); } mDrawerElevation = DRAWER_ELEVATION * density; mNonDrawerViews = new ArrayList<View>(); }
构造函数中,设置
view group的初始焦点,根据手机密度计算出
Drawer的
margin值,初始化从左侧边缘拉出来的布局的回掉监听和从右侧边缘拉出来的布局的回掉监听,其中,在
DrawerLayout的源码是的滑动部分使用的是
ViewDragHelper,所以要初始化左侧的滑动和右侧的滑动,设置触摸时的焦点,初始化view的List
ViewDragHelper的回调ViewDragCallback
其中初始化的过程中有个很重要的方法,就是ViewDragHelper的回掉,下面我们就来看一下
ViewDragCallback
private class ViewDragCallback extends ViewDragHelper.Callback { private final int mAbsGravity; private ViewDragHelper mDragger; private final Runnable mPeekRunable = new Runnable() { @Override public void run() { peekDrawer(); } }; // 注明拖拽的方向 public ViewDragCallback(int gravity) { mAbsGravity = gravity; } public void setDragger(ViewDragHelper dragger) { mDragger = dragger; } // 移除方法回掉 public void removeCallbacks() { DrawerLayout.this.removeCallbacks(mPeekRunnable); } // 当前child是拖拽的view,并且当前拖拽是当前设置的方向,并且当前的child可以拖拽 @Override public boolean tryCaptureView(View child, int pointerId) { // Only capture views where the gravity matches what we're looking for. // This lets us use two ViewDragHelpers, one for each side drawer. return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; } // 当前拖拽的view的状态发生变化时,更新拖拽状态 @Override public void onViewDragStateChanged(int state) { updateDrawerState(mAbsGravity, state, mDragger.getCapturedView()); } // 当view的位置发生变化时,重新布局 @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { float offset; final int childWidth = changedView.getWidth(); // This reverses the positioning shown in onLayout. if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) { offset = (float) (childWidth + left) / childWidth; } else { final int width = getWidth(); offset = (float) (width - left) / childWidth; } setDrawerViewOffset(changedView, offset); changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); invalidate(); } // view开始被拖拽 @Override public void onViewCaptured(View capturedChild, int activePointerId) { final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); lp.isPeeking = false; closeOtherDrawer(); } // 确认当前拖拽的方向,关闭掉其他方向的拖拽 private void closeOtherDrawer() { final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; final View toClose = findDrawerWithGravity(otherGrav); if (toClose != null) { closeDrawer(toClose); } } // 被拖拽的被回掉时调用,先获得子view的宽,然后计算出左边距,滑动到指定位置 @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { // Offset is how open the drawer is, therefore left/right values // are reversed from one another. final float offset = getDrawerViewOffset(releasedChild); final int childWidth = releasedChild.getWidth(); int left; if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT)) { left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; } else { final int width = getWidth(); left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width; } mDragger.settleCapturedViewAt(left, releasedChild.getTop()); invalidate(); } //触摸到边缘时回掉函数 @Override public void onEdgeTouched(int edgeFlags, int pointerId) { postDelayed(mPeekRunnable, PEEK_DELAY); } // 根据拖拽的方向计算出view的左侧位置,判断是哪个方向滑动,如果是单侧划定关闭另一侧的view,取消另一侧的滑动 private void peekDrawer() { final View toCapture; final int childLeft; final int peekDistance = mDragger.getEdgeSize(); final boolean leftEdge = mAbsGravity == Gravity.LEFT; if (leftEdge) { toCapture = findDrawerWithGravity(Gravity.LEFT); childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance; } else { toCapture = findDrawerWithGravity(Gravity.RIGHT); childLeft = getWidth() - peekDistance; } // Only peek if it would mean making the drawer more visible and the drawer isn't locked if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || (!leftEdge && toCapture.getLeft() > childLeft)) && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); lp.isPeeking = true; invalidate(); closeOtherDrawer(); cancelChildViewTouch(); } } // 是否锁定边缘,如果锁定边缘,view不为空并且view不能拖拽,关闭view的抽屉 @Override public boolean onEdgeLock(int edgeFlags) { if (ALLOW_EDGE_LOCK) { final View drawer = findDrawerWithGravity(mAbsGravity); if (drawer != null && !isDrawerOpen(drawer)) { closeDrawer(drawer); } return true; } return false; } // 触摸边缘开始时调用此方法,先根据滑动方向获得当前view,如果当前view可以拖拽,捕获view的操作 @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { final View toCapture; if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { toCapture = findDrawerWithGravity(Gravity.LEFT); } else { toCapture = findDrawerWithGravity(Gravity.RIGHT); } if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { mDragger.captureChildView(toCapture, pointerId); } } // 获取拖拽view的水平方向的范围 @Override public int getViewHorizontalDragRange(View child) { return isDrawerView(child) ? child.getWidth() : 0; } // 捕获水平方向的view被拖拽到的位置 @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { return Math.max(-child.getWidth(), Math.min(left, 0)); } else { final int width = getWidth(); return Math.max(width - child.getWidth(), Math.min(left, width)); } } // 垂直方向view移动的位置 @Override public int clampViewPositionVertical(View child, int top, int dy) { return child.getTop(); } }
ViewDragHelper使用了Scroller,最后滑动的
computeScroll()
@Override public void computeScroll() { final int childCount = getChildCount(); float scrimOpacity = 0; for (int i = 0; i < childCount; i++) { final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; scrimOpacity = Math.max(scrimOpacity, onscreen); } mScrimOpacity = scrimOpacity; // "|" used on purpose; both need to run. if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } }
看过
ViewDragHelper的人应该都知道上面这个方法中的含义,这里简单在代码中注释,详见ViewDragHelper 源码分析
onInterceptTouchEvent方法
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); // "|" used deliberately here; both methods should be invoked. final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | mRightDragger.shouldInterceptTouchEvent(ev); boolean interceptForTap = false; switch (action) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); mInitialMotionX = x; mInitialMotionY = y; if (mScrimOpacity > 0) { final View child = mLeftDragger.findTopChildUnder((int) x, (int) y); if (child != null && isContentView(child)) { interceptForTap = true; } } mDisallowInterceptRequested = false; mChildrenCanceledTouch = false; break; } case MotionEvent.ACTION_MOVE: { // If we cross the touch slop, don't perform the delayed peek for an edge touch. if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) { mLeftCallback.removeCallbacks(); mRightCallback.removeCallbacks(); } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { closeDrawers(true); mDisallowInterceptRequested = false; mChildrenCanceledTouch = false; } } return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch; }
在使用
ViewDragHelper时都知道要拦截事件交给
ViewDragHelper,还有几种情况也要拦截,如果左侧拖转的view不为空,并且
gravity == Gravity.NO_GRAVITY也拦截该事件,在
Down和
Up也拦截该事件
private boolean hasPeekingDrawer() { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); if (lp.isPeeking) { return true; } } return false; }
如果当前的子view是拖拽的view,也拦截该事件
onMeasure方法
由于DrawerLayout是继承自
ViewGroup,所以onMeasure方法主要是计算本身的宽高和子view的宽高,处理设置
wrap_content的情况
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { if (isInEditMode()) { // Don't crash the layout editor. Consume all of the space if specified // or pick a magic number from thin air otherwise. // TODO Better communication with tools of this bogus state. // It will crash on a real device. if (widthMode == MeasureSpec.AT_MOST) { widthMode = MeasureSpec.EXACTLY; } else if (widthMode == MeasureSpec.UNSPECIFIED) { widthMode = MeasureSpec.EXACTLY; widthSize = 300; } if (heightMode == MeasureSpec.AT_MOST) { heightMode = MeasureSpec.EXACTLY; } else if (heightMode == MeasureSpec.UNSPECIFIED) { heightMode = MeasureSpec.EXACTLY; heightSize = 300; } } else { throw new IllegalArgumentException( "DrawerLayout must be measured with MeasureSpec.EXACTLY."); } } setMeasuredDimension(widthSize, heightSize); final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this); final int layoutDirection = ViewCompat.getLayoutDirection(this); // Only one drawer is permitted along each vertical edge (left / right). These two booleans // are tracking the presence of the edge drawers. boolean hasDrawerOnLeftEdge = false; boolean hasDrawerOnRightEdge = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (applyInsets) { final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection); if (ViewCompat.getFitsSystemWindows(child)) { IMPL.dispatchChildInsets(child, mLastInsets, cgrav); } else { IMPL.applyMarginInsets(lp, mLastInsets, cgrav); } } if (isContentView(child)) { // Content views get measured at exactly the layout's size. final int contentWidthSpec = MeasureSpec.makeMeasureSpec( widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); final int contentHeightSpec = MeasureSpec.makeMeasureSpec( heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); child.measure(contentWidthSpec, contentHeightSpec); } else if (isDrawerView(child)) { if (SET_DRAWER_SHADOW_FROM_ELEVATION) { if (ViewCompat.getElevation(child) != mDrawerElevation) { ViewCompat.setElevation(child, mDrawerElevation); } } final @EdgeGravity int childGravity = getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; // Note that the isDrawerView check guarantees that childGravity here is either // LEFT or RIGHT boolean isLeftEdgeDrawer = (childGravity == Gravity.LEFT); if ((isLeftEdgeDrawer && hasDrawerOnLeftEdge) || (!isLeftEdgeDrawer && hasDrawerOnRightEdge)) { throw new IllegalStateException("Child drawer has absolute gravity " + gravityToString(childGravity) + " but this " + TAG + " already has a " + "drawer view along that edge"); } if (isLeftEdgeDrawer) { hasDrawerOnLeftEdge = true; } else { hasDrawerOnRightEdge = true; } final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, mMinDrawerMargin + lp.leftMargin + lp.rightMargin, lp.width); final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, lp.topMargin + lp.bottomMargin, lp.height); child.measure(drawerWidthSpec, drawerHeightSpec); } else { throw new IllegalStateException("Child " + child + " at index " + i + " does not have a valid layout_gravity - must be Gravity.LEFT, " + "Gravity.RIGHT or Gravity.NO_GRAVITY"); } } }
如果宽或者高不是
MeasureSpec.EXACTLY时,如果
widthMode等于
MeasureSpec.AT_MOST,则
widthMode等于
MeasureSpec.EXACTLY,如果
widthMode等于
MeasureSpec.UNSPECIFIED则宽默认等于300,高同理,然后遍历子view
boolean isContentView(View child) { return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; }
如果子view没有设置
gravity属性的话,给子view设置宽高以及mode
boolean isDrawerView(View child) { final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; final int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(child)); if ((absGravity & Gravity.LEFT) != 0) { // This child is a left-edge drawer return true; } if ((absGravity & Gravity.RIGHT) != 0) { // This child is a right-edge drawer return true; } return false; }
判断
gravity属性是
leftor
right,然后通过
child.measure(drawerWidthSpec, drawerHeightSpec);给子view设置宽高Spec
onLayout方法
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mInLayout = true; final int width = r - l; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (isContentView(child)) { child.layout(lp.leftMargin, lp.topMargin, lp.leftMargin + child.getMeasuredWidth(), lp.topMargin + child.getMeasuredHeight()); } else { // Drawer, if it wasn't onMeasure would have thrown an exception. final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); int childLeft; final float newOffset; if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { childLeft = -childWidth + (int) (childWidth * lp.onScreen); newOffset = (float) (childWidth + childLeft) / childWidth; } else { // Right; onMeasure checked for us. childLeft = width - (int) (childWidth * lp.onScreen); newOffset = (float) (width - childLeft) / childWidth; } final boolean changeOffset = newOffset != lp.onScreen; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (vgrav) { default: case Gravity.TOP: { child.layout(childLeft, lp.topMargin, childLeft + childWidth, lp.topMargin + childHeight); break; } case Gravity.BOTTOM: { final int height = b - t; child.layout(childLeft, height - lp.bottomMargin - child.getMeasuredHeight(), childLeft + childWidth, height - lp.bottomMargin); break; } case Gravity.CENTER_VERTICAL: { final int height = b - t; int childTop = (height - childHeight) / 2; // Offset for margins. If things don't fit right because of // bad measurement before, oh well. if (childTop < lp.topMargin) { childTop = lp.topMargin; } else if (childTop + childHeight > height - lp.bottomMargin) { childTop = height - lp.bottomMargin - childHeight; } child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); break; } } if (changeOffset) { setDrawerViewOffset(child, newOffset); } final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE; if (child.getVisibility() != newVisibility) { child.setVisibility(newVisibility); } } } mInLayout = false; mFirstLayout = false; }
遍历子view,如果子view设置了
gravity,根据子view的
gravity属性计算
childLeft和
newOffset,如果子view是垂直方向的,根据
gravity属性计算
topand
bottom
drawChild方法
@Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final int height = getHeight(); final boolean drawingContent = isContentView(child); int clipLeft = 0, clipRight = getWidth(); final int restoreCount = canvas.save(); if (drawingContent) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View v = getChildAt(i); if (v == child || v.getVisibility() != VISIBLE || !hasOpaqueBackground(v) || !isDrawerView(v) || v.getHeight() < height) { continue; } if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) { final int vright = v.getRight(); if (vright > clipLeft) clipLeft = vright; } else { final int vleft = v.getLeft(); if (vleft < clipRight) clipRight = vleft; } } canvas.clipRect(clipLeft, 0, clipRight, getHeight()); } final boolean result = super.drawChild(canvas, child, drawingTime); canvas.restoreToCount(restoreCount); if (mScrimOpacity > 0 && drawingContent) { final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; final int imag = (int) (baseAlpha * mScrimOpacity); final int color = imag << 24 | (mScrimColor & 0xffffff); mScrimPaint.setColor(color); canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); } else if (mShadowLeftResolved != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { final int shadowWidth = mShadowLeftResolved.getIntrinsicWidth(); final int childRight = child.getRight(); final int drawerPeekDistance = mLeftDragger.getEdgeSize(); final float alpha = Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); mShadowLeftResolved.setBounds(childRight, child.getTop(), childRight + shadowWidth, child.getBottom()); mShadowLeftResolved.setAlpha((int) (0xff * alpha)); mShadowLeftResolved.draw(canvas); } else if (mShadowRightResolved != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) { final int shadowWidth = mShadowRightResolved.getIntrinsicWidth(); final int childLeft = child.getLeft(); final int showing = getWidth() - childLeft; final int drawerPeekDistance = mRightDragger.getEdgeSize(); final float alpha = Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f)); mShadowRightResolved.setBounds(childLeft - shadowWidth, child.getTop(), childLeft, child.getBottom()); mShadowRightResolved.setAlpha((int) (0xff * alpha)); mShadowRightResolved.draw(canvas); } return result; }
判断当前的view是否设置
gravity属性值,如果没有设置
gravity,计算
clipLeft和
clipRight值,如果
mScrimOpacity > 0画一个矩形,如果view的
gravity值为
Gravity.LEFT,画右侧的view阴影部分,如果view的
gravity值为
Gravity.RIGHT画左侧的view阴影部分
DrawerLayout的主要功能就是滑动,源码中使用了
ViewDragHelper实现了滑动,具体不了解的地方可以去看
ViewDragHelper源码,
DrawerLayout继承自
ViewGroup,所以要去计算自身以及子view的宽高,以及实现子view在
DrawerLayout的布局,本文主要描述主要的几个方法,想了解其他内容,自行查看源码
以上源码来自api 23,如果有不正确的地方,欢迎指正
相关文章推荐
- 使用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