Launcher3源码浅析(5.1)--Workspace
来源:互联网 发布:雷霆队数据 编辑:程序博客网 时间:2024/06/04 19:42
目录
- 前言
- 初始化
- 布局
- 页面初始化
- 桌面图标
- 图标生成
- 图标拖动
- 图标点击效果
- 页面滑动
前言
Workspace是桌面的主要一个部分,一般设备(如手机)启动起来所看到的桌面的主要界面就是Workspace,在Launcher里其继承关系如下:
Workspace->SmoothPagedView->PagedView->ViewGroup
所以可以说Workspace是一个视图容器类,容器里面主要放插件和应用快捷方式的图标。它负责桌面视图的布局工作,如桌面图标是多少行多少列;用户事件的分发与处理;桌面图标的拖放;子视图的更新等操作。本文简单解析一下Workspace的源码。
初始化
1.布局
1.1.xml布局
其布局在launcher.xml里如下:
<com.android.launcher3.Workspace android:id="@+id/workspace" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" launcher:defaultScreen="@integer/config_workspaceDefaultScreen" />
然后在Launcher.java的onCreate里调用setupViews初始化变量mWorkspace,以便调用Workspace里的一些方法和变量,初始化如下:
mDragLayer = (DragLayer) findViewById(R.id.drag_layer);mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
1.2.参数初始化
Workspace的真正Layout是在DeviceProfile.java里。首先在Launcher.java的onCreate里初始化DeviceProfile,然后调用其layout方法实现布局,如下:
.....DeviceProfile grid = app.initDynamicGrid(this);.....grid.layout(this);
先看看DeviceProfile的初始化过程,调用的是LauncherAppState的initDynamicGrid方法,具体如下:
DeviceProfile initDynamicGrid(Context context) { mDynamicGrid = createDynamicGrid(context, mDynamicGrid); mDynamicGrid.getDeviceProfile().addCallback(this); return mDynamicGrid.getDeviceProfile();}
先调用createDynamicGrid得到mDynamicGrid对象,再通过mDynamicGrid来获取DeviceProfile对象。
LauncherAppState.java里的createDynamicGrid:
static DynamicGrid createDynamicGrid(Context context, DynamicGrid dynamicGrid) { ..... if (dynamicGrid == null) { Point smallestSize = new Point(); Point largestSize = new Point(); display.getCurrentSizeRange(smallestSize, largestSize); dynamicGrid = new DynamicGrid(context, context.getResources(), Math.min(smallestSize.x, smallestSize.y), Math.min(largestSize.x, largestSize.y), realSize.x, realSize.y, dm.widthPixels, dm.heightPixels); } ..... return dynamicGrid;}
DynamicGrid.java的构造函数:
public DynamicGrid(Context context, Resources resources, int minWidthPx, int minHeightPx, int widthPx, int heightPx, int awPx, int ahPx) { DisplayMetrics dm = resources.getDisplayMetrics(); ArrayList<DeviceProfile> deviceProfiles = new ArrayList<DeviceProfile>(); boolean hasAA = !LauncherAppState.isDisableAllApps(); DEFAULT_ICON_SIZE_PX = pxFromDp(DEFAULT_ICON_SIZE_DP, dm); // Our phone profiles include the bar sizes in each orientation deviceProfiles.add(new DeviceProfile("Super Short Stubby", 255, 300, 2, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4)); deviceProfiles.add(new DeviceProfile("Shorter Stubby", 255, 400, 3, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4)); ...... mMinWidth = dpiFromPx(minWidthPx, dm); mMinHeight = dpiFromPx(minHeightPx, dm); mProfile = new DeviceProfile(context, deviceProfiles, mMinWidth, mMinHeight, widthPx, heightPx, awPx, ahPx, resources);}
可以看到根据不同分辨率加载不同的默认布局文件,最后new DeviceProfile对象。
再看看DeviceProfile的构造函数,上面调用了DeviceProfile的两个构造函数,先看第一个:
DeviceProfile(String n, float w, float h, float r, float c, float is, float its, float hs, float his, int dlId) { // Ensure that we have an odd number of hotseat items (since we need to place all apps) if (!LauncherAppState.isDisableAllApps() && hs % 2 == 0) { throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces"); } name = n; minWidthDps = w; minHeightDps = h; numRows = r; numColumns = c; iconSize = is; iconTextSize = its; numHotseatIcons = hs; hotseatIconSize = his; defaultLayoutId = dlId;}
这就是初始化一个页面的一些参数,其中numRows和numColumns对应的是桌面图标的排列分别是几行几列,iconSize就是图标的大小了,看这些变量名基本都能猜到是用来干嘛的了。
第二个构造函数则是根据当前设备找出最接近最合适的一个布局,最终确定一个页面的具体参数。
DeviceProfile(Context context, ArrayList<DeviceProfile> profiles, float minWidth, float minHeight, int wPx, int hPx, int awPx, int ahPx, Resources res) { ..... DeviceProfile closestProfile = findClosestDeviceProfile(minWidth, minHeight, points); // Snap to the closest row count numRows = closestProfile.numRows; // Snap to the closest column count numColumns = closestProfile.numColumns; .....}
1.3.layout实现
接着看看DeviceProfile.java里的layout实现。继续看代码:
public void layout(Launcher launcher) { FrameLayout.LayoutParams lp; ...... // Layout the workspace PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace); lp = (FrameLayout.LayoutParams) workspace.getLayoutParams(); lp.gravity = Gravity.CENTER; int orientation = isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT; Rect padding = getWorkspacePadding(orientation); workspace.setLayoutParams(lp); workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom); workspace.setPageSpacing(getWorkspacePageSpacing(orientation)); .....}
可以看到初始化了workspace,然后设置其布局和间距Padding等。
2.页面初始化
WorkSpace里面可以包含多个页面,一个页面就是一个CellLayout。首先,Launcher在启动的时候,会在LauncherModel里加载桌面图标等资源,加载完成之后,最终会在bindWorkspaceScreens方法里通过回调调用bindScreens方法,而Launcher.java实现了LauncherModel.Callbacks的接口,所以最终调用的是Launcher.java的bindScreens方法。
bindScreens里调用bindAddScreens方法,bindAddScreens里根据实际加载的页面数,循环调用Workspace的insertNewWorkspaceScreenBeforeEmptyScreen方法来生成页面。
for (int i = 0; i < count; i++) { mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen( orderedScreenIds.get(i));}
insertNewWorkspaceScreenBeforeEmptyScreen里又调用insertNewWorkspaceScreen,insertNewWorkspaceScreen方法如下:
public long insertNewWorkspaceScreen(long screenId, int insertIndex) { // Log to disk Launcher.addDumpLog(TAG, "11683562 - insertNewWorkspaceScreen(): " + screenId + " at index: " + insertIndex, true); if (mWorkspaceScreens.containsKey(screenId)) { throw new RuntimeException("Screen id " + screenId + " already exists!"); } CellLayout newScreen = (CellLayout) mLauncher.getLayoutInflater().inflate( R.layout.workspace_screen, null); newScreen.setOnLongClickListener(mLongClickListener); newScreen.setOnClickListener(mLauncher); newScreen.setSoundEffectsEnabled(false); mWorkspaceScreens.put(screenId, newScreen); mScreenOrder.add(insertIndex, screenId); addView(newScreen, insertIndex); return screenId;}
可以看到通过workspace_screen.xml布局实例化了一个CellLayout对象newScreen,最后将该newScreen通过addView方法添加到了Workspace的这个容器里。
桌面图标
1.图标生成
桌面图标其实是一个继承TextView的BubbleTextView对象,其实就是一个小icon加文字的TextView。这个桌面图标的生成可以有三种途径:
①.默认配置生成;
②.从抽屉(或编辑模式里的小部件)里拖动到桌面生成;
③.第三方应用主动生成。
具体途径的各个流程就不分析了,不管是从哪个途径生成的,最终都是调用applyFromShortcutInfo方法来生成最终的图标,所以要改桌面图标风格/样式的都可以在这里修改。具体看下代码:
public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache,boolean setDefaultPadding, boolean promiseStateChanged) { Bitmap b = info.getIcon(iconCache); LauncherAppState app = LauncherAppState.getInstance(); FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b); iconDrawable.setGhostModeEnabled(info.isDisabled != 0); setCompoundDrawables(null, iconDrawable, null, null); if (setDefaultPadding) { DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); setCompoundDrawablePadding(grid.iconDrawablePaddingPx); } if (info.contentDescription != null) { setContentDescription(info.contentDescription); } setText(info.title); setTag(info); if (promiseStateChanged || info.isPromise()) { applyState(promiseStateChanged); }}
可以看到桌面icon是用工具类Utilities生成的FastBitmapDrawable,调用setCompoundDrawables把icon放在Text的上面,setText设置图标文字内容。
2.图标拖动
Launcher里的图标拖动都是由DragController进行控制的,先来看看DragController是怎么控制的。从Launcher.java的onCreate开始,初始化了变量mDragController,然后调用setupViews()进行一些处理:
private void setupViews() { final DragController dragController = mDragController; ..... // Setup the drag layer mDragLayer.setup(this, dragController); ..... // Setup the workspace mWorkspace.setHapticFeedbackEnabled(false); mWorkspace.setOnLongClickListener(this); mWorkspace.setup(dragController); dragController.addDragListener(mWorkspace); // Get the search/delete bar mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.search_drop_target_bar); // Setup AppsCustomize mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane); mAppsCustomizeContent = (AppsCustomizePagedView) mAppsCustomizeTabHost.findViewById( R.id.apps_customize_pane_content); mAppsCustomizeContent.setup(this, dragController); // Setup the drag controller (drop targets have to be added in reverse order in priority) dragController.setDragScoller(mWorkspace); dragController.setScrollView(mDragLayer); dragController.setMoveTarget(mWorkspace); dragController.addDropTarget(mWorkspace); if (mSearchDropTargetBar != null) { mSearchDropTargetBar.setup(this, dragController); mSearchDropTargetBar.setQsbSearchBar(getQsbBar()); } .....
先是对DragLayer对象进行设置,然后把有拖动处理的对象添加到DragController的拖动列表mListeners里。DragLayer实现了对View树改变的监听接口,主要就是拦截触屏事件,然后将事件转到DragController里处理。
图标的拖动有三种情况:
①.在Workspace上进行拖动;
②.从桌面文件夹里拖动到桌面;
③.从抽屉(或编辑模式)里拖动到桌面;
2.1.开始拖动
我们知道拖动事件都是我们长按图标开始的,所以都是从onLongClick方法开始,上面三种情况对应处理:第一种是在Launcher.java的onLongClick里,第二种是在
Folder.java的onLongClick里,第三种则是在AppsCustomizePagedView的onLongClick里。
这三种情况最终都会转到Workspace.beginDragShared方法来处理。那就看看beginDragShared的实现:
public void beginDragShared(View child, DragSource source) { child.clearFocus(); child.setPressed(false); // The outline is used to visualize where the item will land if dropped mDragOutline = createDragOutline(child, DRAG_BITMAP_PADDING); ..... final Bitmap b = createDragBitmap(child, padding); ..... int dragLayerX = Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2); int dragLayerY = Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 - padding.get() / 2); ...... DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale); ......}
先是调用createDragOutline画出拖动时在桌面显示的原图标轮廓Bitmap,接着调用createDragBitmap创建拖动时的图标,然后dragLayerX和dragLayerY是图标拖动时对应的偏移量,最后调用了DragController的startDrag方法开始拖动。
接着看看DragController里的startDrag方法:
public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY, DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,float initialDragViewScale) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } // Hide soft keyboard, if visible if (mInputMethodManager == null) { mInputMethodManager = (InputMethodManager) mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE); } mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); for (DragListener listener : mListeners) { listener.onDragStart(source, dragInfo, dragAction); } final int registrationX = mMotionDownX - dragLayerX; final int registrationY = mMotionDownY - dragLayerY; final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; mDragging = true; mDragObject = new DropTarget.DragObject(); mDragObject.dragComplete = false; mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); mDragObject.dragSource = source; mDragObject.dragInfo = dragInfo; final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale); if (dragOffset != null) { dragView.setDragVisualizeOffset(new Point(dragOffset)); } if (dragRegion != null) { dragView.setDragRegion(new Rect(dragRegion)); } mLauncher.getDragLayer().performHapticFeedback( HapticFeedbackConstants.LONG_PRESS); dragView.show(mMotionDownX, mMotionDownY); handleMoveEvent(mMotionDownX, mMotionDownY); return dragView;}
可以看到循环遍历mListeners了,通知所有之前加进来的拖动监听对象DragListener拖动开始;然后创建拖动对象mDragObject,并设置响应属性;接着创建DragView,并调用其show显示拖动,开始一个属性动画;最后调用handleMoveEvent来移动DragView。
2.2.结束拖动
当用户将被拖拽物移动到相应位置后,将手指从屏幕上移开,此时要处理的就是MotionEvent.ACTION_UP事件,最终在DragController里调用drop方法把拖动对象放到相应位置,调用endDrag()做一些改变变量/释放拖动对象等结束拖动的操作。
private void drop(float x, float y) { final int[] coordinates = mCoordinatesTemp; final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); mDragObject.x = coordinates[0]; mDragObject.y = coordinates[1]; boolean accepted = false; if (dropTarget != null) { mDragObject.dragComplete = true; dropTarget.onDragExit(mDragObject); if (dropTarget.acceptDrop(mDragObject)) { dropTarget.onDrop(mDragObject); accepted = true; } } mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted);}
可以看到drop里先调用findDropTarget查找到当前拖动的对象,如果找到了则把该对象放到最终的位置上。
3.图标点击效果
点击桌面图标,会在BubbleTextView的updateIconState方法里处理点击的效果。
private void updateIconState() { Drawable top = getCompoundDrawables()[1]; if (top instanceof FastBitmapDrawable) { ((FastBitmapDrawable) top).setPressed(isPressed() || mStayPressed); }}
先获取点击的图标,这应该是一个FastBitmapDrawable对象,然后调用FastBitmapDrawable的setPressed方法处理。接着看看FastBitmapDrawable的setPressed方法:
public void setPressed(boolean pressed) { if (mPressed != pressed) { mPressed = pressed; if (mPressed) { mPressedAnimator = ObjectAnimator .ofInt(this, "brightness", PRESSED_BRIGHTNESS) .setDuration(CLICK_FEEDBACK_DURATION); mPressedAnimator.setInterpolator( CLICK_FEEDBACK_INTERPOLATOR); mPressedAnimator.start(); } else if (mPressedAnimator != null) { mPressedAnimator.cancel(); setBrightness(0); } } invalidateSelf();}
可以看到这里使用了属性动画ObjectAnimator,设置的动画的属性是”brightness”,变化范围是0-100,ObjectAnimator在动画的过程中会自动更新属性值,即会调用setBrightness方法。
public void setBrightness(int brightness) { if (mBrightness != brightness) { mBrightness = brightness; updateFilter(); invalidateSelf(); }}
setBrightness里可以看到最终是调用了updateFilter()方法处理点击效果。updateFilter()里最终通过Paint.setColorFilter方法实现了点击时亮度改变的这个效果。
页面滑动
滑动首先是从触摸事件开始,onInterceptTouchEvent拦截触摸事件,onInterceptTouchEvent返回false则事件传递给子view处理,返回true则在PagedView里的onTouchEvent处理。页面滑动就是在PagedView里实现的。所以我们看看PagedView的onTouchEvent处理,主要就是处理down/move/up这三个事件,这个代码比较多,一个个事件来看吧。down这个事件其实没太多处理,就是初始化按下的位置等变量值。move事件就是要获取滑动到的位置然后重新绘制界面了,先看看move事件的处理:
case MotionEvent.ACTION_MOVE: if (mTouchState == TOUCH_STATE_SCROLLING) { // Scroll to follow the motion event final int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) return true; final float x = ev.getX(pointerIndex); final float deltaX = mLastMotionX + mLastMotionXRemainder - x; mTotalMotionX += Math.abs(deltaX); // Only scroll and update mLastMotionX if we have moved some discrete amount. We // keep the remainder because we are actually testing if we've moved from the last // scrolled position (which is discrete). if (Math.abs(deltaX) >= 1.0f) { mTouchX += deltaX; mSmoothingTime = System.nanoTime() / NANOTIME_DIV; if (!mDeferScrollUpdate) { scrollBy((int) deltaX, 0); if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX); } else { invalidate(); } mLastMotionX = x; mLastMotionXRemainder = deltaX - (int) deltaX; } else { awakenScrollBars(); } } .....
从代码可以看到当移动距离deltaX大于等于1时才做滑动处理,调用invalidate()来重新绘制界面。这里PagedView继承ViewGroup,而ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法,而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法。所以接下来会在dispatchDraw里重新绘制界面,如果想实现自己的滑动效果,修改dispatchDraw的实现就可以了。
再看看up事件的处理,up事件即手指离开了屏幕,最后决定是否需要切换页面。
case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_SCROLLING) { final int activePointerId = mActivePointerId; final int pointerIndex = ev.findPointerIndex(activePointerId); final float x = ev.getX(pointerIndex); final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int velocityX = (int) velocityTracker.getXVelocity(activePointerId); final int deltaX = (int) (x - mDownMotionX); final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth(); boolean isSignificantMove = Math.abs(deltaX) > pageWidth * SIGNIFICANT_MOVE_THRESHOLD; mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x); boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING && Math.abs(velocityX) > mFlingThresholdVelocity; ...... if (((isSignificantMove && !isDeltaXLeft && !isFling) || (isFling && !isVelocityXLeft)) && mCurrentPage > 0) { finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; snapToPageWithVelocity(finalPage, velocityX); } else if (((isSignificantMove && isDeltaXLeft && !isFling) ||(isFling && isVelocityXLeft)) &&mCurrentPage < getChildCount() - 1) { finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; snapToPageWithVelocity(finalPage, velocityX); } else { snapToDestination(); } ...... } else if (mTouchState == TOUCH_STATE_PREV_PAGE) { // at this point we have not moved beyond the touch slop // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so // we can just page int nextPage = Math.max(0, mCurrentPage - 1); if (nextPage != mCurrentPage) { snapToPage(nextPage); } else { snapToDestination(); } } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) { // at this point we have not moved beyond the touch slop // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so // we can just page int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1); if (nextPage != mCurrentPage) { snapToPage(nextPage); } else { snapToDestination(); } } ......
首先是页面在滑动(TOUCH_STATE_SCROLLING)的处理,根据isSignificantMove/isDeltaXLeft/isFling/isVelocityXLeft/mCurrentPage这几个变量,确定页面是向左移动还是向右移动,还是留在当前页面。isSignificantMove是判断移动距离是否超过页面宽40%的;isDeltaXLeft是根据移动距离来判断是滑动从左到右还是从右到左;isFling是根据滑动距离和速率,判断是否是滑动;isVelocityXLeft是横向滑动的速率是否大于0;mCurrentPage则是当前页是否超出了页面个数的限制。
页面切换都调用了snapToPage方法,snapToPageWithVelocity方法是根据传进来的whichPage来切换到该页面,snapToDestination方法则是向离屏幕中心最近的页面移动。
接着是不在滑动状态的处理,根据状态是直接切换到上一页(TOUCH_STATE_PREV_PAGE),还是是直接切换到下一页(TOUCH_STATE_NEXT_PAGE)。
接着看看页面切换的方法snapToPage:
protected void snapToPage(int whichPage, int delta, int duration, boolean immediate,TimeInterpolator interpolator) { whichPage = validateNewPage(whichPage); mNextPage = whichPage; ..... mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); updatePageIndicator(); // Trigger a compute() to finish switching pages if necessary if (immediate) { computeScroll(); } // Defer loading associated pages until the scroll settles mDeferLoadAssociatedPagesUntilScrollCompletes = true; mForceScreenScrolled = true; invalidate();}
snapToPage里调用了mScroller.startScroll开始切换操作,完成切换在computeScroll()方法里面,然后会调用scrollTo()方法进行最终的切换。这里面还会调用invalidate()方法进行界面的绘制刷新,形成动画效果和页面切换效果。
博客搬家:http://www.mosiliang.top/?p=168
以后更新都在自个博客上了,有兴趣的可以关注看看,谢谢!
- Launcher3源码浅析(5.1)--Workspace
- Launcher3源码浅析(5.1)--Launcher.java
- Launcher3源码浅析(5.1)--LauncherModel
- Launcher3源码浅析(5.1)--Hotseat
- Launcher3源码浅析(5.1)--OverviewMode
- Launcher3源码分析(Workspace)
- Launcher3源码浅析
- android7.x Launcher3源码解析(3)---workspace和allapps加载流程
- Android M Launcher3主流程源码浅析
- Android M Launcher3主流程源码浅析
- Android M Launcher3主流程源码浅析
- Android M Launcher3主流程源码浅析
- Launcher3源码分析 — 加载Workspace的数据
- Launcher3源码分析 — 加载Workspace的数据
- Launcher3源码分析 — 加载Workspace的数据 .
- Android Launcher3浅析(一)
- Android M Launcher3启动与工作流程源码浅析
- Launcher3的workspace滑动分析
- 进程和线程的区别
- java如何连接数据库Mysql
- 运维工程师的职责以及需要的品质
- Android API Guides---User Interface
- 数组与指针的sizeof大小
- Launcher3源码浅析(5.1)--Workspace
- JavaScript原型链与继承小结
- 基本数值类型转换
- Android消息传递机制---Handler,MessageQueue,Looper.
- web 开发中,直接跳转到刚才的页面
- MYSQL 5.7.12绿色版安装(Windows)
- iOS学习笔记30-系统服务(三)蓝牙
- The type org.springframework.http.converter.HttpMessageConverter cannot be resolved. It is indirectl
- 2016历史周年大事表