仿美团商品选购下拉菜单实现
最后更新于:2022-04-01 14:26:39
感觉自己还是很少写实际应用实现的博客。最近在找实习,写博客时间少了,但还是要坚持。今天这篇博客来讲下电商应用中常见的选择类别下拉列表的实现。先看下效果图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b7dc6e2.jpg)
### 一、下拉列表的实现
其实实现方法有很多,这时实现的也没有什么技术含量,只是总结下自己在项目中的做法,也提供一个思路。
首先是列表的数据,一般数据都是从后台读过来,这里因为没有后台,所以写死在客户端:
~~~
private void initMenuData() {
menuData1 = new ArrayList<Map<String, String>>();
String[] menuStr1 = new String[] { "全部", "粮油", "衣服", "图书", "电子产品",
"酒水饮料", "水果" };
Map<String, String> map1;
for (int i = 0, len = menuStr1.length; i < len; ++i) {
map1 = new HashMap<String, String>();
map1.put("name", menuStr1[i]);
menuData1.add(map1);
}
menuData2 = new ArrayList<Map<String, String>>();
String[] menuStr2 = new String[] { "综合排序", "配送费最低" };
Map<String, String> map2;
for (int i = 0, len = menuStr2.length; i < len; ++i) {
map2 = new HashMap<String, String>();
map2.put("name", menuStr2[i]);
menuData2.add(map2);
}
menuData3 = new ArrayList<Map<String, String>>();
String[] menuStr3 = new String[] { "优惠活动", "特价活动", "免配送费",
"可在线支付" };
Map<String, String> map3;
for (int i = 0, len = menuStr3.length; i < len; ++i) {
map3 = new HashMap<String, String>();
map3.put("name", menuStr3[i]);
menuData3.add(map3);
}
}
~~~
就是做了简单的封装。弹出列表的实现考虑使用Popwindow。
~~~
popMenu = new PopupWindow(contentView,
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
popMenu.setOutsideTouchable(true);
popMenu.setBackgroundDrawable(new BitmapDrawable());
popMenu.setFocusable(true);
popMenu.setAnimationStyle(R.style.popwin_anim_style);
popMenu.setOnDismissListener(new OnDismissListener() {
public void onDismiss() {
productTv.setTextColor(Color.parseColor("#5a5959"));
sortTv.setTextColor(Color.parseColor("#5a5959"));
activityTv.setTextColor(Color.parseColor("#5a5959"));
}
});
~~~
接着将数据封装到adapter中:
~~~
menuAdapter1 = new SimpleAdapter(this, menuData1,
R.layout.item_listview_popwin, new String[] { "name" },
new int[] { R.id.listview_popwind_tv });
menuAdapter2 = new SimpleAdapter(this, menuData2,
R.layout.item_listview_popwin, new String[] { "name" },
new int[] { R.id.listview_popwind_tv });
menuAdapter3 = new SimpleAdapter(this, menuData3,
R.layout.item_listview_popwin, new String[] { "name" },
new int[] { R.id.listview_popwind_tv });
~~~
设置点击标题头弹出列表,并改变标题头的颜色
~~~
public void onClick(View v) {
// TODO Auto-generated method stub
switch (v.getId()) {
case R.id.supplier_list_product:
productTv.setTextColor(Color.parseColor("#39ac69"));
popListView.setAdapter(menuAdapter1);
popMenu.showAsDropDown(product, 0, 2);
menuIndex = 0;
break;
case R.id.supplier_list_sort:
sortTv.setTextColor(Color.parseColor("#39ac69"));
popListView.setAdapter(menuAdapter2);
popMenu.showAsDropDown(product, 0, 2);
menuIndex = 1;
break;
case R.id.supplier_list_activity:
activityTv.setTextColor(Color.parseColor("#39ac69"));
popListView.setAdapter(menuAdapter3);
popMenu.showAsDropDown(product, 0, 2);
menuIndex = 2;
break;
}
}
~~~
showAsDropDown是为了让popwindow定位在Product这个选择标题的正下方。从而实现上面那种方式。
最后完整的贴出代码,还是蛮简单的。最后也会提供代码下载链接。
~~~
public class MainActivity extends Activity implements
OnClickListener {
private ListView listView, popListView;
private ProgressBar progressBar;
private List<Map<String, String>> menuData1, menuData2, menuData3;
private PopupWindow popMenu;
private SimpleAdapter menuAdapter1, menuAdapter2, menuAdapter3;
private LinearLayout product, sort, activity;
private ImageView cartIv;
private TextView productTv, sortTv, activityTv, titleTv;
private int green, grey;
private String currentProduct, currentSort, currentActivity;
private int menuIndex = 0;
private Intent intent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_supplier_list);
findView();
initMenuData();
initPopMenu();
}
private void initMenuData() {
menuData1 = new ArrayList<Map<String, String>>();
String[] menuStr1 = new String[] { "全部", "粮油", "衣服", "图书", "电子产品",
"酒水饮料", "水果" };
Map<String, String> map1;
for (int i = 0, len = menuStr1.length; i < len; ++i) {
map1 = new HashMap<String, String>();
map1.put("name", menuStr1[i]);
menuData1.add(map1);
}
menuData2 = new ArrayList<Map<String, String>>();
String[] menuStr2 = new String[] { "综合排序", "配送费最低" };
Map<String, String> map2;
for (int i = 0, len = menuStr2.length; i < len; ++i) {
map2 = new HashMap<String, String>();
map2.put("name", menuStr2[i]);
menuData2.add(map2);
}
menuData3 = new ArrayList<Map<String, String>>();
String[] menuStr3 = new String[] { "优惠活动", "特价活动", "免配送费",
"可在线支付" };
Map<String, String> map3;
for (int i = 0, len = menuStr3.length; i < len; ++i) {
map3 = new HashMap<String, String>();
map3.put("name", menuStr3[i]);
menuData3.add(map3);
}
}
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
switch (v.getId()) {
case R.id.supplier_list_product:
productTv.setTextColor(Color.parseColor("#39ac69"));
popListView.setAdapter(menuAdapter1);
popMenu.showAsDropDown(product, 0, 2);
menuIndex = 0;
break;
case R.id.supplier_list_sort:
sortTv.setTextColor(Color.parseColor("#39ac69"));
popListView.setAdapter(menuAdapter2);
popMenu.showAsDropDown(product, 0, 2);
menuIndex = 1;
break;
case R.id.supplier_list_activity:
activityTv.setTextColor(Color.parseColor("#39ac69"));
popListView.setAdapter(menuAdapter3);
popMenu.showAsDropDown(product, 0, 2);
menuIndex = 2;
break;
}
}
protected void findView() {
listView = (ListView) findViewById(R.id.supplier_list_lv);
product = (LinearLayout) findViewById(R.id.supplier_list_product);
sort = (LinearLayout) findViewById(R.id.supplier_list_sort);
activity = (LinearLayout) findViewById(R.id.supplier_list_activity);
productTv = (TextView) findViewById(R.id.supplier_list_product_tv);
sortTv = (TextView) findViewById(R.id.supplier_list_sort_tv);
activityTv = (TextView) findViewById(R.id.supplier_list_activity_tv);
titleTv = (TextView) findViewById(R.id.supplier_list_title_tv);
cartIv = (ImageView) findViewById(R.id.supplier_list_cart_iv);
progressBar = (ProgressBar) findViewById(R.id.progress);
product.setOnClickListener(this);
sort.setOnClickListener(this);
activity.setOnClickListener(this);
cartIv.setOnClickListener(this);
}
private void initPopMenu() {
initMenuData();
View contentView = View.inflate(this, R.layout.popwin_supplier_list,
null);
popMenu = new PopupWindow(contentView,
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT);
popMenu.setOutsideTouchable(true);
popMenu.setBackgroundDrawable(new BitmapDrawable());
popMenu.setFocusable(true);
popMenu.setAnimationStyle(R.style.popwin_anim_style);
popMenu.setOnDismissListener(new OnDismissListener() {
public void onDismiss() {
productTv.setTextColor(Color.parseColor("#5a5959"));
sortTv.setTextColor(Color.parseColor("#5a5959"));
activityTv.setTextColor(Color.parseColor("#5a5959"));
}
});
popListView = (ListView) contentView
.findViewById(R.id.popwin_supplier_list_lv);
contentView.findViewById(R.id.popwin_supplier_list_bottom)
.setOnClickListener(new OnClickListener() {
public void onClick(View arg0) {
popMenu.dismiss();
}
});
menuAdapter1 = new SimpleAdapter(this, menuData1,
R.layout.item_listview_popwin, new String[] { "name" },
new int[] { R.id.listview_popwind_tv });
menuAdapter2 = new SimpleAdapter(this, menuData2,
R.layout.item_listview_popwin, new String[] { "name" },
new int[] { R.id.listview_popwind_tv });
menuAdapter3 = new SimpleAdapter(this, menuData3,
R.layout.item_listview_popwin, new String[] { "name" },
new int[] { R.id.listview_popwind_tv });
popListView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long arg3) {
popMenu.dismiss();
if (menuIndex == 0) {
currentProduct = menuData1.get(pos).get("name");
titleTv.setText(currentProduct);
productTv.setText(currentProduct);
Toast.makeText(MainActivity.this, currentProduct, Toast.LENGTH_SHORT).show();
} else if (menuIndex == 1) {
currentSort = menuData2.get(pos).get("name");
titleTv.setText(currentSort);
sortTv.setText(currentSort);
Toast.makeText(MainActivity.this, currentSort, Toast.LENGTH_SHORT).show();
} else {
currentActivity = menuData3.get(pos).get("name");
titleTv.setText(currentActivity);
activityTv.setText(currentActivity);
Toast.makeText(MainActivity.this, currentActivity, Toast.LENGTH_SHORT).show();
}
}
});
}
}
~~~
### 二、加载圆形ProgressBar的显示
就是效果图中的那种加载ProgressBar,圆形ProgresBar可以用原生的Bar来实现,但样式单一,之前我做这种效果第一时间总是考虑到帧动画,但用这种方式需要有很多图片来链接起来,这样一来实现麻烦,二来图片多了占内存。下面用改变原生ProgressBar的动画来实现这种效果,非常简单:
~~~
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminateDrawable="@drawable/shape_progress"
android:indeterminateDuration="1000" />
~~~
indeterminateDrawable是加载背景图片,indeterminateDuration是旋转的速度。这里的思路是用xml来画一张图,它是环形的,且环形圈中有渐变颜色。如下:
~~~
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" >
<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:thicknessRatio="10"
android:useLevel="false" >
<gradient
android:centerColor="#8cd4aa"
android:centerY="0.50"
android:endColor="#ffffff"
android:startColor="#39ac69"
android:type="sweep"
android:useLevel="false" />
</shape>
</rotate>
~~~
rotate设置旋转动画,360度旋转。shape="ring"设置背景为圆。android:innerRadiusRatio="3"设置内环半径,android:thicknessRatio="10"设置外环半径。最后为了让环中颜色有渐变效果,使用gradient来设置。gradient可以有三种渐变方式,线性,辐射,扫描。这里type要设置成扫描。然后设置中心点,开始颜色和结束颜色,就能实现上面的那种效果了。
好了,到这里整个效果就讲完了。贴上源码下载地址:
[https://github.com/reallin/PopWin_MeiTuan](https://github.com/reallin/PopWin_MeiTuan)
Android中Window添加View的底层原理
最后更新于:2022-04-01 14:26:37
### 一,WIndow和windowManager
Window是一个抽象类,它的具体实现是PhoneWindow,创建一个window很简单,只需要创建一个windowManager即可,window具体实现在windowManagerService中,windowManager和windowManagerService的交互是一个IPC的过程。
下面是用windowManager的例子:
~~~
mFloatingButton = new Button(this);
mFloatingButton.setText( "window");
mLayoutParams = new WindowManager.LayoutParams(
LayoutParams. WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0,
PixelFormat. TRANSPARENT);
mLayoutParams. flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
| LayoutParams. FLAG_NOT_FOCUSABLE
| LayoutParams. FLAG_SHOW_WHEN_LOCKED;
mLayoutParams. type = LayoutParams. TYPE_SYSTEM_ERROR;
mLayoutParams. gravity = Gravity. LEFT | Gravity. TOP;
mLayoutParams. x = 100;
mLayoutParams. y = 300;
mFloatingButton.setOnTouchListener( this);
mWindowManager.addView( mFloatingButton, mLayoutParams);
~~~
flags和type两个属性很重要,下面对一些属性进行介绍,首先是flags:
FLAG_NOT_TOUCH_MODAL表示不需要获取焦点,也不需要接收各种输入,最终事件直接传递给下层具有焦点的window。
FLAG_NOT_FOCUSABLE:在此window外的区域单击事件传递到底层window中。当前的区域则自己处理,这个一般都要设置,很重要。
FLAG_SHOW_WHEN_LOCKED :开启可以让window显示在锁屏界面上。
再来看下type这个参数:
window有三种类型:应用window,子window,系统window。应用类对应一个Activity,子Window不能单独存在,需要附属在父Window上,比如常用的Dialog。系统Window是需要声明权限再创建的window,如toast等。
window有z-ordered属性,层级越大,越在顶层。应用window层级1-99,子window1000-1999,系统2000-2999。这此层级对应着windowManager的type参数。系统层级常用的有两个TYPE_SYSTEM_OVERLAY或者TYPE_SYSTEM_ERROR。比如想用TYPE_SYSTEM_ERROR,只需
mLayoutParams.type = LayoutParams.TYPE_SYSTEM_ERROR。还要添加权限。
有了对window的基本认识之后,我们来看下它底层如何实现加载View的。
### 二,window的创建。
其实Window的创建跟之前我写的一篇博客[LayoutInflater源码分析](http://blog.csdn.net/u014486880/article/details/50707672)有点相似。Window的创建是在Activity创建的attach方法中,通过PolicyManager的makeNewWindow方法。Activity中实现了Window的Callback接口,因此当window状态改变时就会回调Activity方法。如onAttachedToWindow等。PolicyManager的真正实现类是Policy,看下它的代码:
~~~
public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
~~~
到此Window创建完成。
下面分析view是如何附属到window上的。看Activity的setContentView方法。
~~~
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
~~~
两部分,设置内容和设置ActionBar。window的具体实现是PhoneWindow,看它的setContent。
~~~
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
~~~
看到了吧,又是分析它。
这里分三步执行:
1.如果没有DecorView,在installDecor中的generateDecor()创建DecorView。之前就分析过,这次就不再分析它了。
2.将View添加到decorview中的mContentParent中。
3.回调Activity的onContentChanged接口。
经过以上操作,DecorView创建了,但还没有正式添加到Window中。在ActivityResumeActivity中首先会调用Activity的onResume,再调用Activity的makeVisible,makeVisible中真正添加view ,代码如下:
~~~
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
~~~
通过上面的addView方法将View添加到Window。
### 三,Window操作View内部机制
## 1.window的添加
一个window对应一个view和一个viewRootImpl,window和view通过ViewRootImpl来建立联系,它并不存在,实体是view。只能通过 windowManager来操作它。
windowManager的实现类是windowManagerImpl。它并没有直接实现三大操作,而是委托给WindowManagerGlobal。addView的实现分为以下几步:
1.检查参数是否合法。
~~~
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent and we're running on L or above (or in the
// system context), assume we want hardware acceleration.
final Context context = view.getContext();
if (context != null
&& context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
~~~
2.创建ViewRootImpl并将View添加到列表中。
~~~
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
~~~
3.通过ViewRootImpl来更新界面并完成window的添加过程 。
~~~
root.setView(view, wparams, panelParentView);
~~~
上面的root就是ViewRootImpl,setView中通过requestLayout()来完成异步刷新,看下requestLayout:
~~~
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
~~~
接下来通过WindowSession来完成window添加过程,WindowSession是一个Binder对象,真正的实现类是 Session,window的添加是一次IPC调用。
~~~
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
}
~~~
在Session内部会通过WindowManagerService来实现Window的添加。
~~~
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outInputChannel);
}
~~~
在WindowManagerService内部会为每一个应用保留一个单独的session。
## 2.window的删除
看下WindowManagerGlobal的removeView:
~~~
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}
~~~
首先调用findViewLocked来查找删除view的索引,这个过程就是建立数组遍历。然后再调用removeViewLocked来做进一步的删除。
~~~
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
if (view != null) {
InputMethodManager imm = InputMethodManager.getInstance();
if (imm != null) {
imm.windowDismissed(mViews.get(index).getWindowToken());
}
}
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
}
~~~
真正删除操作是viewRootImpl来完成的。windowManager提供了两种删除接口,removeViewImmediate,removeView。它们分别表示异步删除和同步删除。具体的删除操作由ViewRootImpl的die来完成。
~~~
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
if (!mIsDrawing) {
destroyHardwareRenderer();
} else {
Log.e(TAG, "Attempting to destroy the window while drawing!\n" +
" window=" + this + ", title=" + mWindowAttributes.getTitle());
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
~~~
由上可知如果是removeViewImmediate,立即调用doDie,如果是removeView,用handler发送消息,ViewRootImpl中的Handler会处理消息并调用doDie。重点看下doDie:
~~~
void doDie() {
checkThread();
if (LOCAL_LOGV) Log.v(TAG, "DIE in " + this + " of " + mSurface);
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
dispatchDetachedFromWindow();
}
if (mAdded && !mFirst) {
destroyHardwareRenderer();
if (mView != null) {
int viewVisibility = mView.getVisibility();
boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
if (mWindowAttributesChanged || viewVisibilityChanged) {
// If layout params have been changed, first give them
// to the window manager to make sure it has the correct
// animation info.
try {
if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
& WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
mWindowSession.finishDrawing(mWindow);
}
} catch (RemoteException e) {
}
}
mSurface.release();
}
}
mAdded = false;
}
WindowManagerGlobal.getInstance().doRemoveView(this);
}
~~~
主要做四件事:
1.垃圾回收相关工作,比如清数据,回调等。
2.通过Session的remove方法删除Window,最终调用WindowManagerService的removeWindow
3.调用dispathDetachedFromWindow,在内部会调用onDetachedFromWindow()和onDetachedFromWindowInternal()。当view移除时会调用onDetachedFromWindow,它用于作一些资源回收。
4.通过doRemoveView刷新数据,删除相关数据,如在mRoot,mDyingViews中删除对象等。
~~~
void doRemoveView(ViewRootImpl root) {
synchronized (mLock) {
final int index = mRoots.indexOf(root);
if (index >= 0) {
mRoots.remove(index);
mParams.remove(index);
final View view = mViews.remove(index);
mDyingViews.remove(view);
}
}
if (HardwareRenderer.sTrimForeground && HardwareRenderer.isAvailable()) {
doTrimForeground();
}
}
~~~
## 3.更新window
看下WindowManagerGlobal中的updateViewLayout。
~~~
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
~~~
通过viewRootImpl的setLayoutParams更新viewRootImpl的layoutParams,接着scheduleTraversals对view重新布局,包括测量,布局,重绘,此外它还会通过WindowSession来更新window。这个过程由WindowManagerService实现。这跟上面类似,就不再重复。到此Window底层源码就分析完啦。
android View绘制源码分析
最后更新于:2022-04-01 14:26:35
在开发过程中我们经常要进行view的自定义。如果熟练掌握自定义技巧的话就能做出很多控件出来。这篇博客来讲讲view绘制背后发生的那些事。
### 一, view的基础知识
## view的绘制概括
首先先说说view绘制的整体过程。
View绘制的源码分析 ,它的三大流程都是在ViewRootImpl中完成的,从ViewRootImpl中的performTraversals开始,有三个方法performMeasure,performLayout,prformDraw分别对measure,layout,draw三个方法。在onMeasure对所有子元素进行measure过程 ,这时measure就从父容器传递到子元素。子元素重复父元素的过程。layout与draw类似,只是draw通过diapatchDraw来实现 。
measure完成后可以通过getMeasureWidth,getMeasureHeight分别获取View测量后的宽高。在实际情况下几乎所有情况它都等于最终宽高。layout过程决定view的四个顶点的坐标和实际view的宽高,完成之后可以通过getTop,getBottom,getLeft,getRight来拿 到view的四个顶点位置。并通过getWidth()和getHeight()来拿到最终宽高。draw决定了view的显示,只有完成才能显示在屏幕上。
## MeasureSpec
在测量过程中系统会将View的LayoutParams根据容器所施加的规则转换成对应的MeasureSpec,然后再根据这个测量出view。
Measure是一个32位的int,高2位代表SpecMode,低30位代表SpecSize。SpecMode表示测量模式,SpecSize指在某种测量模式下规格的大小。其代码如下:
~~~
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK) ;
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK) ;
}
~~~
其实MeasureSpec中源码很值得我们学习。他用一个32位的int来表示模式和大小,节省了空间,也更直观。MeasureSpec通过将specMode和specSize打包成一个int来避免过多的对象内存分配。以上是MeasureSpec的打包和解包过程。
specMode有三种状态:UNSPECIFIED,EXACTLY(相当于match_parent和精确值这两种模式),AT_MOST(wrap_content)。
## LayoutParams
对于一般容器,它的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams共同决定的。上篇博客[LayoutInflater源码解析](http://blog.csdn.net/u014486880/article/details/50707672) 我们己经介绍了android view的结构,PhoneWindow包了一层DecorView,DecorView里才是title和我们的content view。所以行分析DecorView。
先来看下DecorView的产生源码:
~~~
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
~~~
再看下getRootMeasureSpec方法:
~~~
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.//自定义
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
~~~
这里很清楚,分别分MatchPraent和wrap_content和自定义来计算宽高。再来看下普通的view,在ViewGroup的measureChildWIthMargins中:
~~~
protected void measureChildWithMargins (View child,
int parentWidthMeasureSpec , int widthUsed,
int parentHeightMeasureSpec , int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams() ;
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec ,
mPaddingLeft + mPaddingRight + lp. leftMargin + lp.rightMargin
+ widthUsed, lp.width) ;
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec ,
mPaddingTop + mPaddingBottom + lp. topMargin + lp.bottomMargin
+ heightUsed, lp.height) ;
child.measure(childWidthMeasureSpec , childHeightMeasureSpec);
}
~~~
再看下getChildMeasureSpec:
~~~
public static int getChildMeasureSpec(int spec, int padding , int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec. getSize(spec) ;
int size = Math. max( 0, specSize - padding) ;
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec. EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size ;
resultMode = MeasureSpec. EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size ;
resultMode = MeasureSpec. AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension ;
resultMode = MeasureSpec. EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size ;
resultMode = MeasureSpec. AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size ;
resultMode = MeasureSpec. AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension ;
resultMode = MeasureSpec. EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec. UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec. UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode) ;
}
~~~
以上表明,如果父是EXACTLY,parentSize,那么子如果是EXACTLY,
1)具体的值size:那子的MeasureSpec就是EXACTLY,size;
2)MATCH_PARENT:那子的MeasureSpec就是EXACTLY,parentSize;
3)WRAP_CONTENT:那子的MeasureSpec就是AT_MOST,parentSize;
如果父是ATMOST,parentSize,那么子如果是EXACTLY,
1)具体的值size:那子的MeasureSpec就是EXACTLY,size;
2)MATCH_PARENT:那子的MeasureSpec就是AT_MOST,parentSize;
3)WRAP_CONTENT:那子的MeasureSpec就是AT_MOST,parentSize;
总结:对于普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。
## 二,三大过程源码分析
### OnMeasure
* measure。如果是view,measure绘制其自身。如果是VIewGroup,measure绘制自身外,还要绘制其子元素。先看View的measure方法,measure是一个final方法,不能重写:
~~~
if (cacheIndex < 0 |if (cacheIndex < 0 || sIgnoreMeasureCache ) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec , heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}| sIgnoreMeasureCache ) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec , heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
~~~
调用了onMeasure(),来看下它的源码:
~~~
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth() , widthMeasureSpec) ,
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)) ;
}
~~~
看下getSuggestedMinimumWidth(),它就是获取背景大小和mMinWidth的较大值:
~~~
protected int getSuggestedMinimumWidth () {
return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground .getMinimumWidth());
}
~~~
那么mMinWidth是什么呢,mMinWidth就是设置的android:minWidth的属性,没设置就等于0。不信,看如下代码:
~~~
case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr , 0) ;
break;
~~~
getMinimumWidth()表示的是获取背景图大小,它位于Drawable下:
~~~
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight() ;
return intrinsicHeight > 0 ? intrinsicHeight : 0 ;
}
~~~
看下getDefaultSize方法:
~~~
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec. getMode(measureSpec) ;
int specSize = MeasureSpec. getSize(measureSpec) ;
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec. AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
~~~
它返回了specSize,它是测量后的大小。由上面的分析可知,view的宽高由specSize决定,而如果直接继承View的控件需要重写onMeasure方法并设置wrap_content的自身大小。否则wrap_content就相当 于Match_parent了。一般一重写方法如下:
~~~
protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec. AT_MOST
&& heightSpecMode == MeasureSpec. AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec. AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec. AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
~~~
上面的200是指定的一个默认宽高。
2.ViewGroup的measure过程,它没有重写onMeasure,它会调用measureChildren如下:
~~~
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for ( int i = 0 ; i < size; ++i) {
final View child = children[i] ;
if ((child. mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec , heightMeasureSpec);
}
}
}
~~~
分别绘制child,进入measureChild:
~~~
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams() ;
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec ,
mPaddingLeft + mPaddingRight, lp.width) ;
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec ,
mPaddingTop + mPaddingBottom, lp.height) ;
child.measure(childWidthMeasureSpec , childHeightMeasureSpec);
}
~~~
获取LayoutParams,通过getChildMeasureSpec来创建子无素的MeasureSpec,调用child.measure,因为ViewGroup有不同的特性,所以无法实现统一的onMeasure。
## Layout的过程
viewGroup会遍历所有子元素并调用 其layout方法,layout方法来确定子元素的位置。viewgroup如下:
~~~
protected abstract void onLayout(boolean changed,
int l , int t, int r, int b) ;
~~~
需要子类自己实现。看下view的layout:
~~~
public void layout(int l, int t , int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT ) != 0) {
onMeasure(mOldWidthMeasureSpec , mOldHeightMeasureSpec) ;
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical( mParent) ?
setOpticalFrame(l, t , r, b) : setFrame(l, t , r, b);
if (changed || ( mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED ) {
onLayout(changed, l, t , r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li. mOnLayoutChangeListeners .clone();
int numListeners = listenersCopy.size() ;
for ( int i = 0 ; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange( this, l, t, r , b, oldL, oldT , oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
~~~
在setFrame中确定了view的四个顶点坐标。mleft等。onLayout view也没有具体实现,要看具体的。以LinearLayout为例:
~~~
protected void onLayout(boolean changed, int l , int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r , b);
} else {
layoutHorizontal(l, t, r , b);
}
}
~~~
以layoutVertical为例:
~~~
void layoutVertical(int left, int top , int right, int bottom) {
final int paddingLeft = mPaddingLeft ;
int childTop ;
int childLeft ;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight ;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight ;
final int count = getVirtualChildCount() ;
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity. TOP:
default :
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count ; i++) {
final View child = getVirtualChildAt(i) ;
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth() ;
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams() ;
int gravity = lp. gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection() ;
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection) ;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity. CENTER_HORIZONTAL :
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp. leftMargin - lp.rightMargin ;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp. rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp. leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child , childLeft, childTop + getLocationOffset(child) ,
childWidth, childHeight);
childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child , i);
}
}
}
~~~
主要看以下代码:
~~~
final int childWidth = child.getMeasuredWidth() ;
final int childHeight = child.getMeasuredHeight() ;
setChildFrame(child , childLeft, childTop + getLocationOffset(child) ,
childWidth , childHeight);
childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);
~~~
top会逐渐增大,所以会往下排。setChildFrame仅仅是调用子元素的layout方法。
~~~
private void setChildFrame(View child, int left, int top , int width, int height) {
child.layout(left, top, left + width , top + height);
}
~~~
通过子元素的layout来确定自身。
## draw过程
它有以下几步:
* 绘制背景,(canvas)
* 绘制自己。(onDraw)
* 绘制children(dispatchDraw)
* 绘制装饰(onDrawScrollBars)
看下view的draw源码:
~~~
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK ) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo .mIgnoreDirtyState );
mPrivateFlags = (privateFlags & ~ PFLAG_DIRTY_MASK ) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1\. Draw the background
* 2\. If necessary, save the canvas' layers to prepare for fading
* 3\. Draw view's content
* 4\. Draw children
* 5\. If necessary, draw the fading edges and restore layers
* 6\. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL ) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL ) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas) ;
// Step 4, draw the children
dispatchDraw(canvas) ;
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas) ;
if ( mOverlay != null && !mOverlay.isEmpty()) {
mOverlay .getOverlayView().dispatchDraw(canvas) ;
}
// we're done...
return;
}
~~~
viewgroup中的dispatchDraw用于遍历子view并调用子view的draw方法。这样就一层层的传下去。
到此源码分析就结束了。在绘制view的时候经常会在activity中获得view的宽高,因为activity的生命周期和view不同步,在oncreate中无法获取到view的宽高,接下来讲讲activity中如何获取view。
## 三,view宽高确定
* onWindowFocusChanged:view己经初始化完毕,宽高己经准备好。当Activity得到焦点和失去焦点均会被调用,所以它会调用多次。
* 通过view.post,将一个runnable投弟到消息队列尾部,等待looper调用时,view己经初始化好。
~~~
protected void onStart(){
super.onStart();
view.post(new Runnable(){
public void run(){
int width = view.getMeasuredWidth();
int height = new .getMeasuredHeight();
}
})
}
~~~
* ViewTreeObserver:
使用ViewTreeObserver众多回调接口来完成,如OnGlobalLayoutListener,当view树状态发生改变时或内部view可见性发生改变时会回调。
~~~
ViewObserver obserber = view.getViewObserver ();
obserber.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
public void onGlobalLayout(){
obserber.removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = new .getMeasuredHeight();
}
})
~~~
* 通过view进行measure来得到view的宽高。
~~~
int width = MeasureSpec.makeMeasureSpec(100,Measure.EXACTLY);//确定值
int height= MeasureSpec.makeMeasureSpec(100,Measure.EXACTLY);//确定值
view.measure(width,height);
对于wrap_content:
int width = MeasureSpec.makeMeasureSpec((1<<30)-1,Measure.AT_MOST);//wrap_content
int height= MeasureSpec.makeMeasureSpec((1<<30)-1,Measure.AT_MOST);//wrap_content
view.measure(width,height);
~~~
## 四,自定义view中注意事项
自定义View需要注意的事项:
* 如果是继承view或者viewGroup,让view支持wrap_content。
* 如果有必要,让view支持padding。
* view中如果有动画或者线程,要在onDetachedFromWindow中及时停止。当view的Activity退出或者当前view被remove时,调用它。
android测试工具MonkeyRunner–google官网翻译
最后更新于:2022-04-01 14:26:32
最近在复习之前的笔记,在回顾MonkeyRunner时看了看google官网的内容,写得不错,就翻译出来分享下。其实google官网真是一个学习的好地方。
### 基础知识
MonkeyRunner工具提供了一个API用于在Android代码之外控制Android设备和模拟器。通过MonkeyRunner,您可以写出一个Python程序去安装一个Android应用程序或测试包,运行它,向它发送模拟击键,截取它的用户界面图片,并将截图存储于工作站上。monkeyrunner工具的主要设计目的是用于测试功能/框架水平上的应用程序和设备,或用于运行单元测试套件,但您当然也可以将其用于其它目的。
MonkeyRunner工具与monkey工具并无关联。monkey工具直接运行在设备或模拟器的adbshell中,生成用户或系统的伪随机事件流。而MonkeyRunner工具则是在工作站上通过API定义的特定命令和事件控制设备或模拟器。
MonkeyRunner工具为Android测试提供了以下特性:
* 多设备控制:MonkeyRunner的API可以跨多个设备或模拟器实施测试套件。您可以在同一时间接上所有的设备,也可以一次启动全部模拟器,依据程序来依次连接到每个设备,然后运行一个或多个测试。您也可以用程序启动一个配置好的模拟器,运行一个或多个测试。
* 功能测试:MonkeyRunner可以为一个应用自动贯彻一次功能测试。您提供按键或触摸事件的输入数值,然后观察输出结果的截屏。
* 回归测试:MonkeyRunner可以运行某个应用,并将其结果截屏与既定已知正确的结果截屏相比较,以此测试应用的稳定性。
* 可扩展的自动化:由于MonkeyRunner是一个API工具包,您可以基于Python模块和程序开发一整套系统,以此来控制Android设备。除了使用MonkeyRunner的API之外,您还可以使用标准的Python os和subprocess模块来调用如adb这样的Android工具。
您还可以向MonkeyRunnerAPI中添加您自己的类。我们将在后面会对此进行详细讨论。
MonkeyRunner工具使用Jython(使用Java编程语言的一种Python实现)。Jython允许MonkeyRunner API与Android框架轻松的进行交互。使用Jython,您可以使用Python语法来获取API中的常量、类以及方法。
## 一个简单的MonkeyRunner程序实例
以下为一个简单的MonkeyRunner程序,它将会连接到一个设备,创建一个MonkeyDevice对象。使用MonkeyDevice对象,程序将安装一个Android应用包,运行其中一个活动,并向其发送按键事件。程序接下来会将结果截图,创建一个MonkeyImage对象,并使用这个对象截图将保存至.png文件。
~~~
# 导入此程序所需的MonkeyRunner模块
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice
# 连接当前设备,返回一个MonkeyDevice对象
device = MonkeyRunner.waitForConnection()
# 安装Android包,注意,此方法返回的返回值为boolean,由此您可以判断安装过程是否正常
device.installPackage('myproject/bin/MyApplication.apk')
# 运行此应用中的一个活动 device.startActivity(component='com.example.android.myapplication.MainActivity')
# 按下菜单按键
device.press('KEYCODE_MENU','DOWN_AND_UP')
# 截取屏幕截图
result = device.takeSnapShot
# 将截图保存至文件
result.writeToFile('myproject/shot1.png','png')
~~~
## MonkeyRunner 的API
MonkeyRunner 在com.android.monkeyrunner包中总共包含三个模块:
* MonkeyRunner:一个为monkeyrunner程序提供工具方法的类。这个类提供了用于连接monkeyrunner至设备或模拟器的方法。它还提供了用于创建一个monkeyrunner程序的用户界面以及显示内置帮助的方法。
* MonkeyDevice :表示一个设备或模拟器。这个类提供了安装和卸载程序包、启动一个活动以及发送键盘或触摸事件到应用程序的方法。您也可以用这个类来运行测试包。
* MonkeyImage :表示一个截图对象。这个类提供了截图、将位图转换成各种格式、比较两个MonkeyImage对象以及写图像到文件的方法。
在python程序中,您将以Python模块的形式使用这些类。monkeyrunner工具不会自动导入这些模块。您必须使用类似如下的from语句:
~~~
fromcom.android.monkeyrunner import
~~~
其中,为您想要导入的类名。您可以在一个from语句中导入超过一个模块,其间以逗号分隔。
## 运行MonkeyRunner
您可以直接使用一个代码文件运行MonkeyRunner,抑或在交互式对话中输入MonkeyRunner语句。不论使用哪种方式,您都需要调用SDK目录的tools子目录下的MonkeyRunner命令。如果您提供一个文件名作为运行参数,则MonkeyRunner将视文件内容为Python程序,并加以运行;否则,它将提供一个交互对话环境。
MonkeyRunner命令的语法为:
~~~
monkeyrunner -plugin <程序文件名> <程序选项>
~~~
* -plugin : (可选)指定一个内含MonkeyRunner插件的.jar文件。欲了解更多关于monkeyrunner插件的内容,参考下文。要指定多个文件,可以多次使用此参数。
* : 如果您指定此参数,MonkeyRunner将视文件内容为Python程序并予以执行。如果此参数未予指定,则开启一个交互式会话。
* :(可选)中所指定的程序所需的参数。
## MonkeyRunner提供帮助
您可以用以下命令来生成monkeyrunner的API参考:
~~~
monkeyrunner help.py
~~~
参数说明:
可以为text或html,分别代表纯文本和HTML输出。
指定了输出文件的全路径名称。
## 使用插件扩展MonkeyRunner
您可以用Java语言创建新的类,并打包成一个或多个.jar文件,以此来扩展 MonkeyRunner API。您可以使用您自己写的类或者继承现有的类来扩展MonkeyRunner API。您还可以使用此功能来初始化MonkeyRunner环境。
为了使MonkeyRunner加载一个插件,您应当如使用如前面所述的-plugin参数来调用MonkeyRunner命令。
在您编写的插件中,您可以导入或继承位于com.android.monkeyrunner包中的几个主要的monkeyrunner类:MonkeyDevice, MonkeyImage和MonkeyRunner。
请注意,插件无法让你访问Android的SDK。您不能导入com.android.app等包。这是因为monkeyrunner是在框架API层次之下与设备或模拟器进行交互的。
## 插件启动类
用于插件的.jar文件可以指定一个类,使其在脚本执行之前就实例化。如欲指定这个类,您需要在.jar文件的manifest中添加键 MonkeyRunnerStartupRunner。其值为启动时运行的类的名称。以下代码段显示了如何在一个ant构建脚本达到这样的目的:
~~~
<jar jarfile="myplugin" basedir="${build.dir}">
<manifest>
<attribute name="MonkeyRunnerStartupRunner" value="com.myapp.myplugin"/>
</manifest>
</jar>
~~~
如欲访问MonkeyRunner的运行时环境,启动类可以实现com.google.common.base.Predicate。例如,用这个类在默认的命名空间中设置一些变量:
~~~
package com.android.example;
import com.google.common.base.Predicate;
import org.python.util.PythonInterpreter;
public class Main implements Predicate {
@Override
public boolean apply(PythonInterpreter anInterpreter) {
/*
* Examples of creating and initializing variables in the monkeyrunner environment's
* namespace. During execution, the monkeyrunner program can refer to the variables "newtest"
* and "use_emulator"
*
*/
anInterpreter.set("newtest", "enabled");
anInterpreter.set("use_emulator", 1);
return true;
}
}
~~~
MonkeyRunner到此翻译完,用好自动化测试框架对android开发有很大帮助。
android发送邮件
最后更新于:2022-04-01 14:26:30
一个项目的需求,之前一篇博客写过如何生成excel,生成后的excel要发送给用户(指定邮箱)。奇葩的后台说这个发送也不好实现,要客户端来做。也就只能做了(又是分分钟要来个螺旋王)。本篇博客就简单介绍下android如何发送邮件。结果图如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b78d0a2.jpg)
这里我说的只是一种方法,其实发送邮件的方式有很多。在之前的那个生成excel的项目继续扩展。没看过之前项目的可以先看下:[android生成excel](http://blog.csdn.net/u014486880/article/details/50605659)。
首先要导入发送邮件的相应jar包,有三个,分别是activation.jar,additional.jar和mail.jar。这些jar包能在后面的工程中找到。如图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b7a24f0.jpg)
然后写一个发送邮件工具类来写发送邮件的底层代码,先把代码贴出来。
~~~
public class EmailUtil {
class MyAuthenticator extends javax.mail.Authenticator {
private String strUser;
private String strPwd;
public MyAuthenticator(String user, String password) {
this.strUser = user;
this.strPwd = password;
}
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(strUser, strPwd);
}
}
public void sendMail(String toMail, String fromMail, String server,
String username, String password, String title, String body,
String attachment) throws Exception {
Properties props = System.getProperties();// Get system properties
//添加邮箱地址。
props.put("mail.smtp.host", server);// Setup mail server
props.put("mail.smtp.auth", "true");
//添加邮箱权限
MyAuthenticator myauth = new MyAuthenticator(username, password);// Get
Session session = Session.getDefaultInstance(props, myauth);
MimeMessage message = new MimeMessage(session); // Define message
//设置目的邮箱
message.setFrom(new InternetAddress(fromMail)); // Set the from address
message.addRecipient(Message.RecipientType.TO, new InternetAddress(
toMail));// Set
//设置邮件的标题
message.setSubject(title);// Set the subject
// message.setText(MimeUtility.encodeWord(body));// Set the content
MimeMultipart allMultipart = new MimeMultipart("mixed");
MimeBodyPart attachPart = new MimeBodyPart();
//添加附件
FileDataSource fds = new FileDataSource(attachment);
attachPart.setDataHandler(new DataHandler(fds));//附件
attachPart.setFileName(MimeUtility.encodeWord(fds.getName()));
MimeBodyPart textBodyPart = new MimeBodyPart();
//添加邮件内容
textBodyPart.setText(body);
allMultipart.addBodyPart(attachPart);
allMultipart.addBodyPart(textBodyPart);
message.setContent(allMultipart);
message.saveChanges();
Transport.send(message);//发送邮件
}
}
~~~
toMail:是发送目的的邮箱。
fromMail:是用于发送的邮箱。
server:固定写为”smtp.mxhichina.com”。这是阿里云企业邮箱地址。
account:指用于发送邮件的邮箱账号。
password:指用于发送邮件的邮箱密码。
title,body:不用说,就是邮件的题目和内容。
attachment:表示上传的附件在手机中的路径。
代码都加了注释,还是比较清楚的。重点看下添加附件部分,因为内容有两部分,有文字内容和附近,所以要用MimeMultipart 来实现两部分的添加。MimeBodyPart生于添加一部分的内容,setText方法用于添加方字内容。setFileName用于添加附件内容,具体的看代码。
接下来就是发送邮件的业务层代码了。看下MainActivity代码:
~~~
public class MainActivity extends Activity {
Button btn,btn1;
List<Order> orders = new ArrayList<Order>();
String path;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
int length = Const.OrderInfo.orderOne.length;
for(int i = 0;i < length;i++){
Order order = new Order( Const.OrderInfo.orderOne[i][0], Const.OrderInfo.orderOne[i][1], Const.OrderInfo.orderOne[i][2], Const.OrderInfo.orderOne[i][3]);
orders.add(order);
}
btn = (Button)super.findViewById(R.id.btn);
btn1 = (Button)super.findViewById(R.id.btn1);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
String fileName = "excel_"+new Date().toString();
path = "/MailDemo/" + fileName ;
try {
ExcelUtil.writeExcel(MainActivity.this,
orders, path);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
btn1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
String pathString = getExternalFilesDir(null).getPath()+path;
sendMail("635596741@qq.com", "订单",
"邮件由系统自动发送,请不要回复!", pathString+ ".xls");
Toast.makeText(MainActivity.this, "邮件发送成功", Toast.LENGTH_SHORT).show();
}
});
}
public void sendMail(final String toMail, final String title,
final String body, final String path){
new Thread(new Runnable() {
public void run() {
EmailUtil emailUtil = new EmailUtil();
try {
String account = "xxxx@123.com(your mail)";
String password = "xxx(your password)";
emailUtil.sendMail(toMail, account, "smtp.mxhichina.com",
account, password, title, body, path);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
~~~
重点看下sendMail方法,其实很简单,就是把对应的参数往里传,分别传入你的邮箱密码、你要发送的目的邮件、邮件的内容题目及附件路径。但是注意要开启一个线程来发邮件,因为发邮件的时间可能比较长,在主线程中发的话可以会导致NAR。这里还有一点要注意的是传入附件的路径要和excel生成的路径一致,才能找到文件。如果是批量发送邮件,就要考虑使用线程池,但如果是批量发的话,一般都是在后台进行处理。
到此发送邮件的功能就实现了。还是比较简单实用的。
**[项目下载](https://github.com/reallin/Android_Mail)**
LayoutInflater源码解析
最后更新于:2022-04-01 14:26:28
又来一篇源码分析文章。讲源码分析文章有的时候很虚,因为我只能讲个我看懂的大概流程,所以细节部分可以没有深入研究,看完之后也只能了解个大概。但个人觉得看源码更重要的是思路而不是细节。今天来分析下LayoutInflater的源码。
之所以分析它是因为我们来常经常使用到它,但往往只知道它是加载view的而不知它具体的实现方法。不多说直接分析。
### 源码分析
平常我们使用LayoutInflater最常见的方式如:
~~~
LayoutInflater inflate = LayoutInflater.from(Context context); LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
~~~
这两种方式实质上是一样的。看来LayoutInflater.from的源码:
~~~
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context .getSystemService(Context. LAYOUT_INFLATER_SERVICE);
if ( LayoutInflater == null) {
throw new AssertionError( "LayoutInflater not found.");
}
return LayoutInflater;
}
~~~
好吧,它就是一个披着context .getSystemService皮的狼。其实就是简单的封装了下。平常我们在获取系统的一些service如获取传感器之类的都会用到getSystemService,那么在这里context .getSystemService又是具体怎么实现的。
## getSystemService
了解这个问题之前我们要清楚,Context是什么,平时我们经常说Context是上下文环境。其实Application,Activity,Service都会存在一个Context。它的具体实现类是ContextImpl。那么直接看ContextImpl:
~~~
@Override
public Object getSystemService(String name) {
ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
return fetcher == null ? null : fetcher.getService(this);
}
~~~
SYSTEM_SERVICE_MAP是什么。我们继续看ContextImpl的代码:
~~~
class ContextImpl extends Context {
private final static String TAG = "ContextImpl";
private final static boolean DEBUG = false;
/**
* Map from package name, to preference name, to cached preferences.
*/
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
/**
* Override this class when the system service constructor needs a
* ContextImpl. Else, use StaticServiceFetcher below.
*/
/*package*/ static class ServiceFetcher {
int mContextCacheIndex = -1;
/**
* Main entrypoint; only override if you don't need caching.
*/
public Object getService(ContextImpl ctx) {
ArrayList<Object> cache = ctx.mServiceCache;
Object service;
synchronized (cache) {
if (cache.size() == 0) {
// Initialize the cache vector on first access.
// At this point sNextPerContextServiceCacheIndex
// is the number of potential services that are
// cached per-Context.
for (int i = 0; i < sNextPerContextServiceCacheIndex; i++) {
cache.add(null);
}
} else {
service = cache.get(mContextCacheIndex);
if (service != null) {
return service;
}
}
service = createService(ctx);
cache.set(mContextCacheIndex, service);
return service;
}
}
/**
* Override this to create a new per-Context instance of the
* service. getService() will handle locking and caching.
*/
public Object createService(ContextImpl ctx) {
throw new RuntimeException("Not implemented");
}
}
abstract static class StaticServiceFetcher extends ServiceFetcher {
private Object mCachedInstance;
@Override
public final Object getService(ContextImpl unused) {
synchronized (StaticServiceFetcher.this) {
Object service = mCachedInstance;
if (service != null) {
return service;
}
return mCachedInstance = createStaticService();
}
}
public abstract Object createStaticService();
}
private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =
new HashMap<String, ServiceFetcher>();
private static int sNextPerContextServiceCacheIndex = 0;
private static void registerService(String serviceName, ServiceFetcher fetcher) {
if (!(fetcher instanceof StaticServiceFetcher)) {
fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
}
SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
}
static {
…………
registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
}});
…………
}
~~~
在虚拟机第一次加载该类时就会注册各种ServiceFatcher,包括LayoutInflater,这个是在一系列的registerService中实现的。然后将它们存储在SYSTEM_SERVICE_MAP这个HashMap中,以后要用只需从中获取,注册是在静态代码块中进行的,也就是说它只会执行一次,保证实例的唯一性。这可以说是用容器来实现的单例模式。最后通过getSystemService获取相应的Service。
我们重点关注到PolicyManager.makeNewLayoutInflate这句代码,它最终调用的是Policy中的makeNewLayoutInflate方法。
~~~
public class Policy implements IPolicy {
private static final String TAG = "PhonePolicy";
static {
// For performance reasons, preload some policy specific classes when
// the policy gets loaded.
for (String s : preload_classes) {
try {
Class.forName(s);
} catch (ClassNotFoundException ex) {
Log.e(TAG, "Could not preload class for phone policy: " + s);
}
}
}
public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
public LayoutInflater makeNewLayoutInflater(Context context) {
return new PhoneLayoutInflater(context);
}
}
~~~
在这里看到Window具体实现类是PhoneWindow,LayoutInflater的具体实现是PhoneLayoutInflater。
## PhoneLayoutInflater
看下PhoneLayoutInflater的代码。
~~~
public class PhoneLayoutInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
/**
* Instead of instantiating directly, you should retrieve an instance
* through {@link Context#getSystemService}
*
* @param context The Context in which in which to find resources and other
* application-specific things.
*
* @see Context#getSystemService
*/
public PhoneLayoutInflater(Context context) {
super(context);
}
protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
}
/** Override onCreateView to instantiate names that correspond to the
widgets known to the Widget factory. If we don't find a match,
call through to our super class.
*/
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
}
~~~
它重定了onCreateView 方法,其实就是为系统View加上相应的前缀。如TextView读出的完整路径会是android.widget.TextView。再调用createView方法,通过类的完整路径来构造View对象。具体的实现过程我们可以来看看setContentView。
## setContentView
首先我们来看一幅android的结构图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b778a02.jpg)
Acitivity的一个界面中最外层是PhoneWindow,它里面是一个DecorView。它是界面中所有view的根view。它包括两部分,ActionBar和ContentView,而ContentView就是我们平常接触最多的,setContentView设置的就是它的内容。而我们在xml中定义的所有view都是显示在它上面的。
先来看下PhoneWindow中的setContentView方法。
~~~
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
~~~
首先调用的是installDecor方法。从名字中我们就可以猜到,它应该就是加载DecorView。看下代码:
~~~
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
mDecorContentParent = decorContentParent;
mDecorContentParent.setWindowCallback(getCallback());
if (mDecorContentParent.getTitle() == null) {
mDecorContentParent.setWindowTitle(mTitle);
}
final int localFeatures = getLocalFeatures();
for (int i = 0; i < FEATURE_MAX; i++) {
if ((localFeatures & (1 << i)) != 0) {
mDecorContentParent.initFeature(i);
}
}
mDecorContentParent.setUiOptions(mUiOptions);
if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 ||
(mIconRes != 0 && !mDecorContentParent.hasIcon())) {
mDecorContentParent.setIcon(mIconRes);
} else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 &&
mIconRes == 0 && !mDecorContentParent.hasIcon()) {
mDecorContentParent.setIcon(
getContext().getPackageManager().getDefaultActivityIcon());
mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK;
}
if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 ||
(mLogoRes != 0 && !mDecorContentParent.hasLogo())) {
mDecorContentParent.setLogo(mLogoRes);
}
// Invalidate if the panel menu hasn't been created before this.
// Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
// being called in the middle of onCreate or similar.
// A pending invalidation will typically be resolved before the posted message
// would run normally in order to satisfy instance state restoration.
PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
if (!isDestroyed() && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_ACTION_BAR);
}
} else {
mTitleView = (TextView)findViewById(R.id.title);
if (mTitleView != null) {
mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
View titleContainer = findViewById(
R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
} else {
mTitleView.setText(mTitle);
}
}
}
if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);
}
// Only inflate or create a new TransitionManager if the caller hasn't
// already set a custom one.
if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
if (mTransitionManager == null) {
final int transitionRes = getWindowStyle().getResourceId(
R.styleable.Window_windowContentTransitionManager,
0);
if (transitionRes != 0) {
final TransitionInflater inflater = TransitionInflater.from(getContext());
mTransitionManager = inflater.inflateTransitionManager(transitionRes,
mContentParent);
} else {
mTransitionManager = new TransitionManager();
}
}
mEnterTransition = getTransition(mEnterTransition, null,
R.styleable.Window_windowEnterTransition);
mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION,
R.styleable.Window_windowReturnTransition);
mExitTransition = getTransition(mExitTransition, null,
R.styleable.Window_windowExitTransition);
mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION,
R.styleable.Window_windowReenterTransition);
mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null,
R.styleable.Window_windowSharedElementEnterTransition);
mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition,
USE_DEFAULT_TRANSITION,
R.styleable.Window_windowSharedElementReturnTransition);
mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null,
R.styleable.Window_windowSharedElementExitTransition);
mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition,
USE_DEFAULT_TRANSITION,
R.styleable.Window_windowSharedElementReenterTransition);
if (mAllowEnterTransitionOverlap == null) {
mAllowEnterTransitionOverlap = getWindowStyle().getBoolean(
R.styleable.Window_windowAllowEnterTransitionOverlap, true);
}
if (mAllowReturnTransitionOverlap == null) {
mAllowReturnTransitionOverlap = getWindowStyle().getBoolean(
R.styleable.Window_windowAllowReturnTransitionOverlap, true);
}
if (mBackgroundFadeDurationMillis < 0) {
mBackgroundFadeDurationMillis = getWindowStyle().getInteger(
R.styleable.Window_windowTransitionBackgroundFadeDuration,
DEFAULT_BACKGROUND_FADE_DURATION_MS);
}
if (mSharedElementsUseOverlay == null) {
mSharedElementsUseOverlay = getWindowStyle().getBoolean(
R.styleable.Window_windowSharedElementsUseOverlay, true);
}
}
}
}
~~~
可以看到这里不仅初始化mContentParent,而且在之前先调用generateDecor();初始化了一个mDecor,mDecor是DecorView对象,为FrameLayout的子类。并通过findViewById进行获取控件。
generateLayout(mDecor)猜测应该是用来获取到了我们的ContentView的;具体我们来看下源码。
## generateLayout
~~~
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//...... //依据主题style设置一堆值进行设置
int layoutResource;
int features = getLocalFeatures();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null)
{
throw new RuntimeException("Window couldn't find content container view");
} //...... //继续一堆属性设置,完事返回contentParent
return contentParent;
}
~~~
之前我们设置界面主题(如Notitle)主要有两种方式,一种是在xml中设置:
~~~
android:theme="@android:style/Theme.Black.NoTitleBar"
~~~
一种是调用requestFeature。
~~~
requestWindowFeature(Window.FEATURE_NO_TITLE);
~~~
第一种方法对应的就是getWindowStyle方法。而第二种就是在getLocalFeatures完成的。这就是为什么requestWindowFeature要写在setContentView方法之前的原因。
接着通过mDecor.findViewById传入R.id.content,返回mDecor(布局)中的id为content的View,就是我们前面说的ContentView。可以看到我们的mDecor是一个FrameLayout,然后会根据theme去选择系统中的布局文件,将布局文件通过inflate转化为view,加入到mDecor中;这些布局文件中都包含一个id为content的FrameLayout,将其引用返回给mContentParent。
等我们的mContentParent有值了以后,再次回到setContentView中,它会调用以下方法。
~~~
mLayoutInflater.inflate(layoutResID, mContentParent);
~~~
接下来重点看inflate是如何加载布局的。
## inflate
~~~
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context)mConstructorArgs[0];
mConstructorArgs[0] = mContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, attrs, false, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, attrs, false);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp
rInflate(parser, temp, attrs, true, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (IOException e) {
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
}
~~~
代码很长,主要分为以下几步:
1.解析XML的根标签。
2.如果是merge,调用rInflate进行解析。它会把merge所有子view添加到根标签中。
3.如果是普通标签,调用createViewFromTag进行解析。
4.调用rInflate解析temp根元素下的子view。并添加到temp中。
最后返回root。
看下createViewFromTag是如何加载标签的。
~~~
View createViewFromTag(View parent, String name, AttributeSet attrs, boolean inheritContext) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
Context viewContext;
if (parent != null && inheritContext) {
viewContext = parent.getContext();
} else {
viewContext = mContext;
}
// Apply a theme wrapper, if requested.
final TypedArray ta = viewContext.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
viewContext = new ContextThemeWrapper(viewContext, themeResId);
}
ta.recycle();
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(viewContext, attrs);
}
if (DEBUG) System.out.println("******** Creating view: " + name);
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, viewContext, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, viewContext, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, viewContext, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
if (DEBUG) System.out.println("Created view is: " + view);
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
}
}
~~~
上面这个方法中,如果传过来的名字没有“.”,会认为是一个内置view。会调用onCreateView来解析这个View。PhoneLayoutInflater的onCreateView就是为内置的View加上前缀,如android.widget等。然后再调用createView()来进行view的构造。
~~~
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
//constructor是从缓存中取出的构造函数
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//如果prefix不为空就构造view路径并加载
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
//通过反射构造view
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object[] args = mConstructorArgs;
args[1] = attrs;
constructor.setAccessible(true);
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} catch (NoSuchMethodException e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (prefix != null ? (prefix + name) : name));
ie.initCause(e);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View "
+ (prefix != null ? (prefix + name) : name));
ie.initCause(e);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()));
ie.initCause(e);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
~~~
又是一大堆代码,但它其实就是使用view的完整路径将类加载到虚拟机中,通过构造函数来创建view对象,这个过程是通过反射。最后返回view。这个就解析了单个view。那如果是一棵树,则交给rInflate来处理。看下代码:
~~~
void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
boolean finishInflate, boolean inheritContext) throws XmlPullParserException,
IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, parent, attrs, inheritContext);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, attrs, inheritContext);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflate(parser, view, attrs, true, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) parent.onFinishInflate();
}
/**
* Parses a <code><request-focus></code> element and requests focus on
* the containing View.
*/
private void parseRequestFocus(XmlPullParser parser, View view)
throws XmlPullParserException, IOException {
int type;
view.requestFocus();
final int currentDepth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
// Empty
}
}
~~~
普希金通过深度优先遍历来构造视图树。每解析一个view就会递归调用rInflate。view的结构一层包一层,其实就是标准的组合设计模式实现的。整个视图构建完后就会在onResume之后,内容就会出现在界面中。
到此,LayoutInflater加载View的过程就分析完了。还是比较简单易懂的。
### 布局加载优化
通过上面的分析,我们可以提出几点常用的布局优化手段:
1.尽量使用相对布局,减少不必要层级结构。
2.使用merge属性。使用它可以有效的将某些符合条件的多余的层级优化掉,使用merge标签还是有一些限制的,具体是:merge只能用在布局XML文件的根元素;使用merge来inflate一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置inflate的attachToRoot参数为true。;不能在ViewStub中使用merge标签。
3.使用ViewStub。一个轻量级的页面,我们通常使用它来做预加载处理,来改善页面加载速度和提高流畅性,ViewStub本身不会占用层级,它最终会被它指定的层级取代。ViewStub也是有一些缺点,譬如:ViewStub只能Inflate一次,之后ViewStub对象会被置为空。VIewStub中不能嵌套merge标签。
4.使用include。这个标签是为了布局重用。
当然还有很多优化手段和工具可以使用,在此不一一罗列。关于布局还是有很大的学问要去深入研究的。
Volley源码解析
最后更新于:2022-04-01 14:26:25
很早之前就想写下关于Volley的源码解析。一开始学android网络访问都是使用HttpClient,刚接触么Volley的时候就瞬间爱不释手,虽说现在项目中使用OkHttp多些(Volley更新慢),但是作为google自家推出的网络框架,Volley还是有很多值得学习的地方。这篇博客是我对Volley源码分析后的一个总结。
### Volley的使用
Volley的使用非常简单,相信大家都很熟悉。首先需要获取到一个RequestQueue对象。
~~~
RequestQueue mQueue = Volley.newRequestQueue(context);
~~~
如果想通过网络获取json,如下:
~~~
StringRequest stringRequest = new StringRequest("http://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d("TAG", response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("TAG", error.getMessage(), error);
}
});
~~~
只要在onResponse中处理返回的response即可。如果访问出错,则会调用onErrorResonse方法。 注意Volley是异步,是在子线程中进行网络访问,而onResponse里的代码是在主线程中执行。所以使用Volley的地方切记不要把它当成单线程,这是初学者经常犯错的地方。最后,将这个StringRequest对象添加到RequestQueue里面就可以了。
~~~
mQueue.add(stringRequest);
~~~
如果要加载图片,则首先要定义一个ImageCache,用于定义图片的缓存。通过ImageLoader来加载图片,ImageListener则用于指定ImageView以及加载失败和加载过程中默认图片 :
~~~
private final LruCache<String, Bitmap> mLruCache = new LruCache<String, Bitmap>(
(int) (Runtime.getRuntime().maxMemory() / 10))
{
@Override
protected int sizeOf(String key, Bitmap value)
{
return value.getRowBytes() * value.getHeight();
}
};
@Override
public void putBitmap(String url, Bitmap bitmap)
{
mLruCache.put(url, bitmap);
}
@Override
public Bitmap getBitmap(String url)
{
return mLruCache.get(url);
}
});
ImageListener listener = ImageLoader.getImageListener(imageView,
R.drawable.default, R.drawable.failed);
imageLoader.get(imageurl, listener);
~~~
介绍完简单用法之后,就来分析源代码了。
### Volley源码分析
先看下官网给出的介绍图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b75a47d.jpg)
这里我们先有个大概的介绍,蓝色是主线程,绿色是CacheDispatcher(硬盘缓存)线程,红色是NetworkDispatcher(网络请求线程)。我们在主线程中调用RequestQueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送HTTP请求,解析响应结果,写入缓存,并回调主线程。接下来详细的进行分析。
不用说,入口肯定是Volley.newRequestQueue(context)。先看下newRequestQueue的代码:
~~~
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
userAgent = packageName + "/" + info. versionCode;
} catch (NameNotFoundException e) {
}
if (stack == null) {
if (Build.VERSION. SDK_INT >= 9) {
stack = new HurlStack();
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
stack = new HttpClientStack(AndroidHttpClient. newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue( new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
}
~~~
首先封装得到userAgent,User-Agent 字段设置为 App 的packageName/{versionCode},如果异常则使用 “volley/0”。上面代码主要是实例化stack ,如果SDK版本大于9,使用HurlStack,否则使用HttpClientStack。实际上HurlStack的内部就是使用HttpURLConnection进行网络通讯的,而HttpClientStack的内部则是使用HttpClient进行网络通讯的。也就是说android2.2以上的都是使用HttpURLConnection,否则使用HttpClient。接着new了一个RequestQueue,并调用它的start方法。来看下它的RequestQueue构造方法:
~~~
/** Number of network request dispatcher threads to start. */
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
/** Cache interface for retrieving and storing responses. */
private final Cache mCache;
/** Network interface for performing requests. */
private final Network mNetwork;
/** Response delivery mechanism. */
private final ResponseDelivery mDelivery;
/** The network dispatchers. */
private NetworkDispatcher[] mDispatchers;
public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}
~~~
初始化主要就是4个参数:mCache、mNetwork、mDispatchers、mDelivery。第一个是硬盘缓存;第二个主要用于Http相关操作;第三个用于转发请求的;第四个参数用于把结果转发到UI线程,通过它来对外声明接口。接下来看下start方法。
~~~
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
/** Cache interface for retrieving and storing responses. */
private final Cache mCache;
/** Network interface for performing requests. */
private final Network mNetwork;
/** Response delivery mechanism. */
private final ResponseDelivery mDelivery;
/** The network dispatchers. */
private NetworkDispatcher[] mDispatchers;
/**
* Starts the dispatchers in this queue.
*/
public void start() {
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the pool size.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
/**
* Stops the cache and network dispatchers.
*/
public void stop() {
if (mCacheDispatcher != null) {
mCacheDispatcher.quit();
}
for (int i = 0; i < mDispatchers.length; i++) {
if (mDispatchers[i] != null) {
mDispatchers[i].quit();
}
}
}
~~~
首先调用stop()方法,确保此时所有转发器都处于停止状态。接下来就new了一个CacheDispatcher转发器,它其实就是一个线程,用于硬盘缓存。再new了四个NetworkDispatcher转发器,用于网络请求。并分别调用这些线程的start()方法。如果是加载图片,我们还需定义一个imageLoader,来看看Volley中为我们定义的ImageLoader,主要看它的get方法:
~~~
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight) {
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
// Try to look up the request in the cache of remote images.
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// The bitmap did not exist in the cache, fetch it!
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// Update the caller to let them know that they should use the default bitmap.
imageListener.onResponse(imageContainer, true);
// Check to see if a request is already in-flight.
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
}
// The request is not already in flight. Send the new request to the network and
// track it.
Request<?> newRequest =
new ImageRequest(requestUrl, new Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
onGetImageSuccess(cacheKey, response);
}
}, maxWidth, maxHeight,
Config.RGB_565, new ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onGetImageError(cacheKey, error);
}
});
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
~~~
上面代码具体流程是这样,首先通过throwIfNotOnMainThread()方法限制必须在UI线程调用;然后根据传入的参数计算cacheKey,获取缓存;如果存在cache,直接将返回结果封装为一个ImageContainer,然后直接回调imageListener.onResponse(container, true);这时我们就可以设置图片了。如果不存在,那就初始化一个ImageContainer,然后直接回调imageListener.onResponse(imageContainer, true),这里是为了让我们设置默认图片。所以,在实现listener的时候,要先判断resp.getBitmap()是否为null;接下来检查该url是否早已加入了请求对了,如果已加入,则将刚初始化的ImageContainer加入BatchedImageRequest。这就是加载图片时的内存缓存。
然后调用RequestQueue的add()方法将Request传入就可以完成网络请求操作了,让我们来看看add方法中到底做了什么事。
~~~
private final Map<String, Queue< Request<?>>> mWaitingRequests =
new HashMap<String, Queue< Request<?>>>();
/**
* The set of all requests currently being processed by this RequestQueue. A Request
* will be in this set if it is waiting in any queue or currently being processed by
* any dispatcher.
*/
private final Set<Request <?>> mCurrentRequests = new HashSet<Request<?>>();
/** The cache triage queue. */
private final PriorityBlockingQueue< Request<?>> mCacheQueue =
new PriorityBlockingQueue< Request<?>>();
/** The queue of requests that are actually going out to the network. */
private final PriorityBlockingQueue< Request<?>> mNetworkQueue =
new PriorityBlockingQueue< Request<?>>();
/**
* Adds a Request to the dispatch queue.
* @param request The request to service
* @return The passed -in request
*/
public <T> Request<T> add(Request<T> request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
request.setRequestQueue( this);
synchronized ( mCurrentRequests) {
mCurrentRequests.add(request);
}
// Process requests in the order they are added.
request.setSequence(getSequenceNumber());
request.addMarker( "add-to-queue");
// If the request is uncacheable, skip the cache queue and go straight to the network.
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
// Insert request into stage if there's already a request with the same cache key in flight.
synchronized ( mWaitingRequests) {
String cacheKey = request.getCacheKey();
if ( mWaitingRequests.containsKey(cacheKey)) {
// There is already a request in flight. Queue up.
Queue<Request<?>> stagedRequests = mWaitingRequests .get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<Request<?>>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog. DEBUG) {
VolleyLog. v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
}
} else {
// Insert 'null' queue for this cacheKey, indicating there is now a request in
// flight.
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
~~~
这里首先将请求加入mCurrentRequests,这个mCurrentRequests是一个HashSet,它保存了所有需要处理的Request,主要为了提供cancel的入口。如果该请求不应该被缓存则直接加入mNetworkQueue,然后返回。request.shouldCache()在默认情况下,每条请求都是可以缓存的,当然我们也可以调用Request的setShouldCache(false)方法来改变这一默认行为。
接下来判断该请求是否有相同的请求正在被处理,如果有则加入mWaitingRequests;如果没有,则加入mWaitingRequests.put(cacheKey, null),并将request加入到CacheQueue中。
有了队列,我们就来看看线程是如何执行的。先看CacheDispatcher。
~~~
public class CacheDispatcher extends Thread {
private static final boolean DEBUG = VolleyLog.DEBUG;
/** The queue of requests coming in for triage. */
private final BlockingQueue<Request<?>> mCacheQueue;
/** The queue of requests going out to the network. */
private final BlockingQueue<Request<?>> mNetworkQueue;
/** The cache to read from. */
private final Cache mCache;
/** For posting responses. */
private final ResponseDelivery mDelivery;
/** Used for telling us to die. */
private volatile boolean mQuit = false;
/**
* Creates a new cache triage dispatcher thread. You must call {@link #start()}
* in order to begin processing.
*
* @param cacheQueue Queue of incoming requests for triage
* @param networkQueue Queue to post requests that require network to
* @param cache Cache interface to use for resolution
* @param delivery Delivery interface to use for posting responses
*/
public CacheDispatcher(
BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
Cache cache, ResponseDelivery delivery) {
mCacheQueue = cacheQueue;
mNetworkQueue = networkQueue;
mCache = cache;
mDelivery = delivery;
}
/**
* Forces this dispatcher to quit immediately. If any requests are still in
* the queue, they are not guaranteed to be processed.
*/
public void quit() {
mQuit = true;
interrupt();
}
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// Make a blocking call to initialize the cache.
mCache.initialize();
while (true) {
try {
// Get a request from the cache triage queue, blocking until
// at least one is available.
final Request<?> request = mCacheQueue.take();
request.addMarker("cache-queue-take");
// If the request has been canceled, don't bother dispatching it.
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
// Attempt to retrieve this item from cache.
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// Cache miss; send off to the network dispatcher.
mNetworkQueue.put(request);
continue;
}
// If it is completely expired, just send it to the network.
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
// We have a cache hit; parse its data for delivery back to the request.
request.addMarker("cache-hit");
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()) {
// Completely unexpired cache hit. Just deliver the response.
mDelivery.postResponse(request, response);
} else {
// Soft-expired cache hit. We can deliver the cached response,
// but we need to also send the request to the network for
// refreshing.
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// Mark the response as intermediate.
response.intermediate = true;
// Post the intermediate response back to the user and have
// the delivery then forward the request along to the network.
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Not much we can do about this.
}
}
});
}
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
}
}
}
~~~
我们要知道CacheDispatcher是硬盘缓存,到此可知Volley也是有二级缓存的。重点看它的run方法。看到while(true)时,我们就知道,它是在不断的执行的。首先从mCacheQueue中取出缓存,如果没有取到,就把它加入mNetworkQueue中,再判断缓存是否过期,如果过期,也放入mNetworkQueue中。否则就取到了可用的缓存了,再调用request.parseNetworkResponse解析从缓存中取出的data和responseHeaders通过mDelivery.postResponse转发,然后回调到UI线程;我们看下mDelivery.postResponse方法:
~~~
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
request.markDelivered();
request.addMarker("post-response");
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
~~~
主要看ResponseDeliveryRunnable。
~~~
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
mRequest = request;
mResponse = response;
mRunnable = runnable;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
// If this request has canceled, finish it and don't deliver.
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}
// Deliver a normal response or error, depending.
if (mResponse.isSuccess()) {
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}
// If this is an intermediate response, add a marker, otherwise we're done
// and the request can be finished.
if (mResponse.intermediate) {
mRequest.addMarker("intermediate-response");
} else {
mRequest.finish("done");
}
// If we have been provided a post-delivery runnable, run it.
if (mRunnable != null) {
mRunnable.run();
}
}
~~~
在它的run方法中,如果访问成功会调用mRequest.deliverResponse(mResponse.result)方法,到这里就很明了了,因为每个request子类中都要重写deliverResponse,最后我们再在这个方法中将响应的数据回调到Response.Listener的onResponse()方法中就可以了。以StringRequest为例:
~~~
@Override
protected void deliverResponse(String response) {
mListener.onResponse (response);
}
~~~
分析完缓存,我们来看下网络加载。它是在NetworkDispatcher线程中实现的。
~~~
public class NetworkDispatcher extends Thread {
/** The queue of requests to service. */
private final BlockingQueue<Request<?>> mQueue;
/** The network interface for processing requests. */
private final Network mNetwork;
/** The cache to write to. */
private final Cache mCache;
/** For posting responses and errors. */
private final ResponseDelivery mDelivery;
/** Used for telling us to die. */
private volatile boolean mQuit = false;
/**
* Creates a new network dispatcher thread. You must call {@link #start()}
* in order to begin processing.
*
* @param queue Queue of incoming requests for triage
* @param network Network interface to use for performing requests
* @param cache Cache interface to use for writing responses to cache
* @param delivery Delivery interface to use for posting responses
*/
public NetworkDispatcher(BlockingQueue<Request<?>> queue,
Network network, Cache cache,
ResponseDelivery delivery) {
mQueue = queue;
mNetwork = network;
mCache = cache;
mDelivery = delivery;
}
@Override
public void run() {
Process.setThreadPriority(Process. THREAD_PRIORITY_BACKGROUND);
Request<?> request;
while ( true) {
try {
// Take a request from the queue.
request = mQueue.take();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if ( mQuit) {
return;
}
continue;
}
try {
request.addMarker( "network-queue-take" );
// If the request was cancelled already, do not perform the
// network request.
if (request.isCanceled()) {
request.finish( "network-discard-cancelled" );
continue;
}
addTrafficStatsTag(request);
// Perform the network request.
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker( "network-http-complete" );
// If the server returned 304 AND we delivered a response already,
// we're done -- don't deliver a second identical response.
if (networkResponse. notModified && request.hasHadResponseDelivered()) {
request.finish( "not-modified");
continue;
}
// Parse the response here on the worker thread.
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker( "network-parse-complete" );
// Write to cache if applicable.
// TODO: Only update cache metadata instead of entire record for 304s.
if (request.shouldCache() && response. cacheEntry != null ) {
mCache.put(request.getCacheKey(), response.cacheEntry );
request.addMarker( "network-cache-written" );
}
// Post the response back.
request.markDelivered();
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog. e(e, "Unhandled exception %s", e.toString());
mDelivery.postError(request, new VolleyError(e));
}
}
}
}
~~~
首先取出请求;然后通过mNetwork.performRequest(request)处理我们的请求,拿到NetworkResponse。看下performRequest方法:
~~~
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
Map<String, String> responseHeaders = new HashMap<String, String>();
try {
// Gather headers.
Map<String, String> headers = new HashMap<String, String>();
addCacheHeaders(headers, request.getCacheEntry());
httpResponse = mHttpStack.performRequest(request, headers);
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
responseHeaders = convertHeaders(httpResponse.getAllHeaders());
// Handle cache validation.
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
request.getCacheEntry() == null ? null : request.getCacheEntry().data,
responseHeaders, true);
}
// Some responses such as 204s do not have content. We must check.
if (httpResponse.getEntity() != null) {
responseContents = entityToBytes(httpResponse.getEntity());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
responseContents = new byte[0];
}
// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents, statusLine);
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
} catch (Exception e) {
……
}
}
}
~~~
上面方法主要是网络请求的一些细节,所以如果要修改请求的细节就要到此处修改(后面会讲到)。
在这里服务器会返回的数据组装成一个NetworkResponse对象进行返回。在NetworkDispatcher中收到了NetworkResponse这个返回值后又会调用Request的parseNetworkResponse()方法来解析NetworkResponse中的数据,再将数据写入到缓存。parseNetworkResponse的实现是交给Request的子类来完成的,不同种类的Request解析的方式不同。如json与gson就有区别。最后与CacheDispatcher一样调用mDelivery.postResponse(request, response)返回回调,这里就不再分析了。
到这里volley的源码就分析完了,总结一下:
* 首先初始化RequestQueue,主要就是开启CacheDispatcher和NetworkDispatcher线程,线程会不断读取请求,没有消息则阻塞。
* 当我们发出请求以后,会根据url,ImageView属性等,构造出一个cacheKey,然后首先从LruCache中获取,这个缓存我们自己构建的,这就是内存缓存;如果没有取到,则判断是否存在硬盘缓存,这一步是从getCacheDir里面获取(默认5M);如果没有取到,则从网络请求;
### Volley的扩展
## 添加cookie头
volley跟httpClient不一样,它是不会自动添加cookie头的。但是cookie在应用中却很重要,它会保证登陆后的操作都处于一个会话中,有效的增加了安全性。那么如何在volley中自动添加cookie呢。
首先在新建Appliaction,当成全局的Application,然后在里面编写在http头参数中识别出cookie和添加cookie到Http头代码。
~~~
/**
* Checks the response headers for session cookie and saves it
* if it finds it.
* @param headers Response Headers.
*/
public static final void checkSessionCookie(Map<String, String> headers) {
Log.e("TAG", "checkSessionCookie->headers:" + headers);
if (headers.containsKey(GlobalParams.SET_COOKIE_KEY) && headers.get(GlobalParams.SET_COOKIE_KEY).startsWith(GlobalParams.SESSION_COOKIE)) {
String cookie = headers.get(GlobalParams.SET_COOKIE_KEY);
if (cookie.length() > 0) {
//形如Set-Cookie:JSESSIONID=18D6BCC01453C6EB39BB0C4208F389EE; Path=/smdb
//进行解析,取出JSESSIONID的value
String[] splitCookie = cookie.split(";");
String[] splitSessionId = splitCookie[0].split("=");
cookie = splitSessionId[1];
Editor prefEditor = preferences.edit();
prefEditor.putString(GlobalParams.SESSION_COOKIE, cookie);
prefEditor.commit();
}
}else {
if (null != httpclient.getCookieStore()) {
List<Cookie> cookies = httpclient.getCookieStore().getCookies();
for (Cookie cookie : cookies) {
if ("JSESSIONID".equals(cookie.getName())) {//取得session的value
String sessionId = cookie.getValue();
Editor prefEditor = preferences.edit();
prefEditor.putString(GlobalParams.SESSION_COOKIE, sessionId);
prefEditor.commit();
break;
}
}
if (!cookies.isEmpty()) {
for (int i = 0; i < cookies.size(); i++) {
cookie = cookies.get(i);//保存cookie的信息使得HttpClient和WebView共享同一个cookie
}
}
}
}
}
~~~
接着就要在Request的子类中合适地方添加头信息,哪个地方合适。我们来看下HurlStack的performRequest方法。
~~~
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
HashMap<String, String> map = new HashMap<String, String>();
map.putAll(request.getHeaders());
map.putAll(additionalHeaders);
if (mUrlRewriter != null) {
String rewritten = mUrlRewriter.rewriteUrl(url);
if (rewritten == null) {
throw new IOException("URL blocked by rewriter: " + url);
}
url = rewritten;
}
URL parsedUrl = new URL(url);
HttpURLConnection connection = openConnection(parsedUrl, request);
for (String headerName : map.keySet()) {
connection.addRequestProperty(headerName, map.get(headerName));
}
setConnectionParametersForRequest(connection, request);
// Initialize HttpResponse with data from the HttpURLConnection.
ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
int responseCode = connection.getResponseCode();
if (responseCode == -1) {
// -1 is returned by getResponseCode() if the response code could not be retrieved.
// Signal to the caller that something was wrong with the connection.
throw new IOException("Could not retrieve response code from HttpUrlConnection.");
}
StatusLine responseStatus = new BasicStatusLine(protocolVersion,
connection.getResponseCode(), connection.getResponseMessage());
BasicHttpResponse response = new BasicHttpResponse(responseStatus);
response.setEntity(entityFromConnection(connection));
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
if (header.getKey() != null) {
Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
response.addHeader(h);
}
}
return response;
}
~~~
重点看到map.putAll(request.getHeaders());所以我们考虑到如果要给它添加头信息可以在request的getHeaders()方法中添加。至此我们以StringRequest为例,重写一个类叫MyStringRequest:
~~~
public class MyStringRequest extends StringRequest {
private final Map<String, String> mParams;
/**
* @param method
* @param url
* @param params
* A {@link HashMap} to post with the request. Null is allowed
* and indicates no parameters will be posted along with request.
* @param listener
* @param errorListener
*/
public MyStringRequest(int method, String url, Map<String, String> params, Listener<String> listener,
ErrorListener errorListener) {
super(method, url, listener, errorListener);
mParams = params;
}
@Override
protected Map<String, String> getParams() {
return mParams;
}
/* (non-Javadoc)
* @see com.android.volley.toolbox.StringRequest#parseNetworkResponse(com.android.volley.NetworkResponse)
*/
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
// since we don't know which of the two underlying network vehicles
// will Volley use, we have to handle and store session cookies manually
Log.e("TAG", "parseNetworkResponse->response.headers:" + response.headers);
GlobalApplication.checkSessionCookie(response.headers);
return super.parseNetworkResponse(response);
}
/* (non-Javadoc)
* @see com.android.volley.Request#getHeaders()
*/
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = super.getHeaders();
if (headers == null || headers.equals(Collections.emptyMap())) {
headers = new HashMap<String, String>();
}
GlobalApplication.addSessionCookie(headers);
return headers;
}
}
~~~
在parseNetworkResponse中调用checkSessionCookie解析头信息中的cookie,然后重写getHeaders方法,调用addSessionCookie添加cookie。
## 添加重定向功能
网络访问经常要用到重定向,虽说在客户端中用得比较少。那Volley能不能进行自动重定向,答案是可以的,重要修改下源码。既然要重定向,那就要在请求返回的进行判断,毫无疑问要在BasicNetwork的performRequest中修改,先看下修改后的代码:
~~~
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
Map<String, String> responseHeaders = new HashMap<String, String>();
try {
// Gather headers.
Map<String, String> headers = new HashMap<String, String>();
addCacheHeaders(headers, request.getCacheEntry());
httpResponse = mHttpStack.performRequest(request, headers);
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
responseHeaders = convertHeaders(httpResponse.getAllHeaders());
// Handle cache validation.
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
request.getCacheEntry() == null ? null : request.getCacheEntry().data,
responseHeaders, true);
}
// Handle moved resources
//Line143-148为解决301/302重定向问题增加的代码。
//参考见https://github.com/elbuild/volley-plus/commit/4a65a4099d2b1d942f4d51a6df8734cf272564eb#diff-b4935f77d9f815bb7e0dba85e55dc707R150
if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
String newUrl = responseHeaders.get("Location");
request.setRedirectUrl(newUrl);
}
// Some responses such as 204s do not have content. We must check.
if (httpResponse.getEntity() != null) {
responseContents = entityToBytes(httpResponse.getEntity());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
responseContents = new byte[0];
}
// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents, statusLine);
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
} catch (SocketTimeoutException e) {
attemptRetryOnException("socket", request, new TimeoutError());
} catch (ConnectTimeoutException e) {
attemptRetryOnException("connection", request, new TimeoutError());
} catch (MalformedURLException e) {
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
int statusCode = 0;
NetworkResponse networkResponse = null;
if (httpResponse != null) {
statusCode = httpResponse.getStatusLine().getStatusCode();
} else {
throw new NoConnectionError(e);
}
//Line143-148为解决301/302重定向问题增加的代码。
if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY ||
statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl());
} else {
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
}
if (responseContents != null) {
networkResponse = new NetworkResponse(statusCode, responseContents,
responseHeaders, false);
if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
statusCode == HttpStatus.SC_FORBIDDEN) {
attemptRetryOnException("auth",
request, new AuthFailureError(networkResponse));
} else if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY ||
statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
//else if语句为解决301/302重定向问题增加的代码。设置重连请求。
attemptRetryOnException("redirect",
request, new AuthFailureError(networkResponse));
} else {
// TODO: Only throw ServerError for 5xx status codes.
throw new ServerError(networkResponse);
}
} else {
throw new NetworkError(networkResponse);
}
}
}
}
~~~
其实重点添加了以下的代码:
~~~
if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
String newUrl = responseHeaders.get("Location");
request.setRedirectUrl(newUrl);
}
~~~
上面的代码就是判断返回code是否是301或302,如果是就获取重定向的Url,再设置重定向,很简单。到此Volley常见的扩展功能就讲完了。
源码解析的文章都会有点长,写完也要有耐心。继续坚持。
android中生成excel
最后更新于:2022-04-01 14:26:23
都说程序员不爽产品经理,其实有的时候遇到一些奇葩的后台开发人员也会很不顺心。最近项目有这样一个要求,要生成一个excel然后发邮件给客户。结果后台人员直接把这个功能扔给客户端,理由是后台不好实现。听到这也就只能自己实现了(分分钟就想来个螺旋王扣它头上)。这篇博客讲下如下在android中生成excel表并存到本地。先看下生成后的效果图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b747b1f.jpg)
## 初始化数据
首先我们要先造下测试数据,这里我把数据写死在一个常量类Const中,如下:
~~~
public class Const {
public interface OrderInfo{
public static final String[][] orderOne = new String[][] {{ "123", "九龙", "13294352311",
"武汉市关山口" },{ "124", "咱家", "13294352312",
"武汉市水果湖" },{ "125", "陈家", "13294352315",
"武汉市华师" },{ "126", "李", "13294352316",
"武汉市杨家湾" }};
}
}
~~~
理论上这些数据是从后台读过来的。
本文模拟打印订单的信息,所以这里还需要一个订单Model类:
~~~
public class Order implements Serializable {
public String id;
public String restPhone;
public String restName;
public String receiverAddr;
public Order(String id,String restPhone, String restName, String receiverAddr) {
this.id = id;
this.restPhone = restPhone;
this.restName = restName;
this.receiverAddr = receiverAddr;
}
}
~~~
## 存内存卡
接下来我们要判断一下内存卡是否存在,内存是否足够大。先获取指定目录下内存的大小:
~~~
/** 获取SD可用容量 */
private static long getAvailableStorage(Context context) {
String root = context.getExternalFilesDir(null).getPath();
StatFs statFs = new StatFs(root);
long blockSize = statFs.getBlockSize();
long availableBlocks = statFs.getAvailableBlocks();
long availableSize = blockSize * availableBlocks;
// Formatter.formatFileSize(context, availableSize);
return availableSize;
}
~~~
这里用到的路径是getExternalFilesDir,它指定的是SDCard/Android/data/你的应用的包名/files/ 目录这个目录,它用来放一些长时间保存的数据,当应用被卸载时,会同时会删除。类似这种情况的还有getExternalCacheDir方法,只是它一般用来存放临时文件。之后通过StatFs来计算出可用容量的大小。
接下来在写入excel前对内存进行判断,如下:
~~~
if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)&&getAvailableStorage()>1000000) {
Toast.makeText(context, "SD卡不可用", Toast.LENGTH_LONG).show();
return;
}
File file;
File dir = new File(context.getExternalFilesDir(null).getPath());
file = new File(dir, fileName + ".xls");
if (!dir.exists()) {
dir.mkdirs();
}
~~~
如果内存卡不存在或内存小于1M,不进行写入,然后创建相应的文件夹并起名字。接下来重点看下如何写入excel。
## 生成写入excel
* 导入相关包
这里需要导入jxl包,它主要就是用于处理excel的,这个包我会附在项目放在github中,后面会给出链接。
* 生成excel工作表
以下代码是在指定路径下生成excel表,此时只是一个空表。
~~~
WritableWorkbook wwb;
OutputStream os = new FileOutputStream(file);
wwb = Workbook.createWorkbook(os);
~~~
* 添加sheet表
熟悉excel操作的都知道excel可以新建很多个sheet表。以下代码生成第一个工作表,名字为“订单”:
~~~
WritableSheet sheet = wwb.createSheet("订单", 0);
~~~
* 添加excel表头
添加excel的表头,这里可以自定义表头的样式,先看代码:
~~~
String[] title = { "订单", "店名", "电话", "地址" };
Label label;
for (int i = 0; i < title.length; i++) {
// Label(x,y,z) 代表单元格的第x+1列,第y+1行, 内容z
// 在Label对象的子对象中指明单元格的位置和内容
label = new Label(i, 0, title[i], getHeader());
// 将定义好的单元格添加到工作表中
sheet.addCell(label);
}
~~~
这里表头信息我写死了。表的一个单元格对应一个Label,如label(0,0,”a”)代表第一行第一列所在的单元格信息为a。getHeader()是自定义的样式,它返回一个 WritableCellFormat 。来看看如何自定义样式:
~~~
public static WritableCellFormat getHeader() {
WritableFont font = new WritableFont(WritableFont.TIMES, 10,
WritableFont.BOLD);// 定义字体
try {
font.setColour(Colour.BLUE);// 蓝色字体
} catch (WriteException e1) {
e1.printStackTrace();
}
WritableCellFormat format = new WritableCellFormat(font);
try {
format.setAlignment(jxl.format.Alignment.CENTRE);// 左右居中
format.setVerticalAlignment(jxl.format.VerticalAlignment.CENTRE);// 上下居中
format.setBorder(Border.ALL, BorderLineStyle.THIN,
Colour.BLACK);// 黑色边框
format.setBackground(Colour.YELLOW);// 黄色背景
} catch (WriteException e) {
e.printStackTrace();
}
return format;
}
~~~
看上面代码就很清楚了,通过获得WritableFont 来自定义字体的一些样式,如颜色大小等,通过WritableCellFormat 来设置文本框的样式,可以设置边框底色等。具体的可以查api文档,这里只给出例子。
* 添加excel内容。
~~~
for (int i = 0; i < exportOrder.size(); i++) {
Order order = exportOrder.get(i);
Label orderNum = new Label(0, i + 1, order.id);
Label restaurant = new Label(1, i + 1, order.restName);
Label nameLabel = new Label(2,i+1,order.restPhone);
Label address = new Label(3, i + 1, order.receiverAddr);
sheet.addCell(orderNum);
sheet.addCell(restaurant);
sheet.addCell(nameLabel);
sheet.addCell(address);
Toast.makeText(context, "写入成功", Toast.LENGTH_LONG).show();
}
~~~
这就再简单不过了,就是获取order信息然后一个个写入。
## 主类
这个demo布局很简单就是一个按钮点击来生成excel,这里就不贴出来了,MainActivity的代码如下:
~~~
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
int length = Const.OrderInfo.orderOne.length;
for(int i = 0;i < length;i++){
Order order = new Order( Const.OrderInfo.orderOne[i][0], Const.OrderInfo.orderOne[i][1], Const.OrderInfo.orderOne[i][2], Const.OrderInfo.orderOne[i][3]);
orders.add(order);
}
btn = (Button)super.findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
try {
ExcelUtil.writeExcel(MainActivity.this,
orders, "excel_"+new Date().toString());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
~~~
这里我把上面生成excel的代码封装到一个工具类ExcelUtil中,以后使用就直接调用 。MainActivity就是将数组组装到Order中调用ExcelUtil来写入。到此android实现excel功能就实现了。
[**源码地址**](https://github.com/reallin/Android_Excel)
android TextView实现跑马灯效果
最后更新于:2022-04-01 14:26:21
最近忙着项目,很久没有总结提交博客和提交github了。接下来我打算整理下项目中用到的比较有用的发表到博客上。也打算总结一些关于设计模式和源码分析的博客。今天的话就先来讲下一个非常简单但又很常用的控件,跑马灯状态的TextView。当我的要显示的文本长度太长,又不想换行时用它来显示文本一来可以完全的显示出文本,二来效果也挺酷,实现起来超级简单,所以,何乐不为。先看下效果图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b70c60b.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b72437c.jpg)
## 代码实现
TextView自带了跑马灯功能,只要把它的ellipsize属性设置为marquee就可以了。但有个前提,就是TextView要处于被选中状态才能有效果,看到这,我们就很自然的自定义一个控件,写出以下代码:
~~~
public class MarqueeTextView extends TextView {
public MarqueeTextView(Context con) {
super(con);
}
public MarqueeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MarqueeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean isFocused() {
// TODO Auto-generated method stub
if(getEditableText().equals(TruncateAt.MARQUEE)){
return true;
}
return super.isFocused();
}
}
~~~
重写了isFocused方法,并进行判断,只有设置了marqueen属性的才保持选中状态,否则它就跟普通TextView一样。接下来就可以直接使用了,看下布局:
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/titlebar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#39ac69" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#ffffff"
android:gravity="center_vertical"
android:orientation="horizontal" >
<ImageView
android:id="@+id/home_location_iv"
android:layout_width="25dp"
android:layout_height="27dp"
android:layout_marginLeft="10dp"
android:scaleType="fitXY"
android:src="@drawable/icon_place" />
<com.lxj.marqueetextview.MarqueeTextView
android:id="@+id/home_location_tv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_weight="1"
android:ellipsize="marquee"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:singleLine="true"
android:text="正在定位..."
android:textColor="#39ac69"
android:textSize="18sp" />
<ImageView
android:id="@+id/home_search_iv"
android:layout_width="25dp"
android:layout_height="27dp"
android:layout_marginRight="10dp"
android:scaleType="fitXY"
android:src="@drawable/icon_place" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
~~~
要注意两点ellipsize属性要设置为”marquee”,行数属性即singleLine要设置为true。到此TextView的跑马灯效果就实现了。
Data Binding Guide——google官方文档翻译(下)
最后更新于:2022-04-01 14:26:18
这篇博客是Data Binding Guide官网文档翻译的下篇,如果没看过前半部分翻译的可以先看[Data Binding Guide——google官方文档翻译(上)](http://blog.csdn.net/u014486880/article/details/50508133)
### 一,数据对象
任何不含业务逻辑的java简单对象(POJO)可用于数据绑定,但修改POJO不能使UI更新。而通过数据绑定可以使数据对象感知到数据的变化。有三种不同的感知数据改变的机制,可见对象,可见字段,和可见集合。
当一个可见数据对象绑定到用户界面和数据对象变化的属性时,用户界面将自动更新。
### 可见对象
一个实现可见接口的类,允许把监听器和对象绑定,以便监听该对象的所有属性的变化。
可见接口有添加和删除侦听器的机制,但数据改变的通知机制取决于开发者。为了使开发更简洁,创建一个基类(BaseObservable)来实现监听器注册机制。当属性改变时,数据实现类仍负责告知机制,其中getter方法加@Bindable注释,并在setter方法中告知属性的变化。
~~~
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
~~~
在编译过程中,绑定注释在BR类文件中生成一个条目,然后在模块包中生成BR类文件。如果数据类的基类不可改变,可以使用方便PropertyChangeRegistry有效地存储和通知侦听器的方式来实现可见接口。
### 可见字段
创建可见类的过程中,开发人员想节省时间或有很多属性时可以使用可见字段,例如一些常用的可见类型字段:[ObservableBoolean](http://developer.android.com/reference/android/databinding/ObservableBoolean.html), [ObservableByte](http://developer.android.com/reference/android/databinding/ObservableByte.html), [ObservableChar](http://developer.android.com/reference/android/databinding/ObservableChar.html),[ObservableShort](http://developer.android.com/reference/android/databinding/ObservableShort.html), [ObservableInt](http://developer.android.com/reference/android/databinding/ObservableInt.html), [ObservableLong](http://developer.android.com/reference/android/databinding/ObservableLong.html), [ObservableFloat](http://developer.android.com/reference/android/databinding/ObservableFloat.html), [ObservableDouble](http://developer.android.com/reference/android/databinding/ObservableDouble.html), and[ObservableParcelable](http://developer.android.com/reference/android/databinding/ObservableParcelable.html)。
可见字段是有独立字段的可见对象。原始版本在访问操作中避免装箱和拆箱操作。为方便使用,在数据类创建使用public final修饰的字段。
~~~
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
~~~
通过setter和getter方法获取值。
~~~
user.firstName.set("Google");
int age = user.age.get();
~~~
可见集合
一些应用程序使用更多的动态结构来保存数据。可见集合支持键控存取方式访问这些数据对象。当键是一个像字符串这样的引用类型时,可使用ObservableArrayMap。
~~~
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
~~~
在布局中,可以通过引用String类型的键进行映射:
~~~
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap<String, Object>"/>
</data>
…
<TextView
android:text='@{user["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user["age"])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
~~~
当键是整型时,可使用[ObservableArrayList](http://developer.android.com/reference/android/databinding/ObservableArrayList.html)。
~~~
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
~~~
在布局中,可通过索引访问列表。
~~~
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
~~~
### 生成绑定
任何不含业务逻辑的java简单对象(POJO)可用于数据绑定,但修改POJO不能使UI更新。而通过数据绑定可以使数据对象感知到数据的变化。有三种不同的感知数据改变的机制,可见对象,可见字段,和可见集合。
当一个可见数据对象绑定到用户界面和数据对象变化的属性时,用户界面将自动更新。
生成绑定类链接在布局视图的布局变量,正如前面所讨论的,绑定的名称和包可以自定义。生成的绑定类都继承ViewDataBinding。
应该立即创建绑定,以确保布局中的表达式与视图的绑定不干扰视图层。有几种绑定到布局的方法。最常见的是在绑定类调用静态方法inflate。inflate解析视图,并完成数据绑定。还有一个更简单的版本,只需要在inflate方法中引入LayoutInflater或再加上ViewGroup:
~~~
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
~~~
如果布局inflate机制有变化,也可以使用分开绑定的机制。
~~~
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
~~~
在无法预先知道是否绑定的情况下,可以使用[DataBindingUtil](http://developer.android.com/reference/android/databinding/DataBindingUtil.html) 类创建绑定:
~~~
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
~~~
### 有ID的视图
在布局中每个有ID的视图将相应生成一个public final字段的变量,绑定类只调用一次布局,并创建每个视图。这种机制比调用findViewById方法更有效率。
~~~
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:id="@+id/firstName"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:id="@+id/lastName"/>
</LinearLayout>
</layout>
~~~
会生成带有如下属性的绑定类:
~~~
public final TextView firstName;
public final TextView lastName;
~~~
ID可能没有数据绑定那样必要,但是仍有一些实例证明,从代码访问角度看仍然是必要的。
### 二、变量
每个变量会给出存取方法:
~~~
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
~~~
在绑定中生成setter和getter方法:
~~~
public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);
~~~
### ViewStubs
viewstubs与普通视图不同。他们开始是是一个不可视并且大小为0的视图,可以延迟到运行时填充布局资源。设置为Visible或调用inflate()之后,就会填充布局资源,ViewStub便会被填充的视图替代。
由于ViewStub会在视图层消失,为了正常的连接,对应的绑定对象也要随之消失。由于视图层是final类型的,ViewStubProxy对象替代ViewStub 后,开发者可以访问 ViewStub,并且当 ViewStub 在视图层中被加载时,开发者也可以访问加载的视图。
当解析其他布局时,新的布局中要建立绑定关系。因此,ViewStubProxy 对象要监听[ViewStub的OnInflateListener](http://developer.android.com/reference/android/view/ViewStub.OnInflateListener.html)并建立绑定。开发者可以在ViewStubProxy 对象上建立一个OnInflateListener,绑定建立后,便可调用OnInflateListener。
### 高级绑定
### 动态变量
有时具体的绑定类会不被识别。例如,操作任意布局的[RecyclerView.Adapter](http://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html)必须在onBindViewHolder中指定绑定值,才可识别出对应的绑定类。
在这个例子中,RecyclerView绑定的所有布局都有一个item变量。BindingHolder 对象通过引用getBinding() 方法获取 [ViewDataBinding](http://developer.android.com/reference/android/databinding/ViewDataBinding.html) 基类。
~~~
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
~~~
### 立即绑定
当变量变化时,绑定在下一帧到来前也要随之改变。然而,当绑定需要立即执行时,可以调用强制执行方法[executePendingBindings()](#)。
### 后台线程
可以在后台线程中改变数据模型,只要它不是集合。数据绑定将每个变量或字段保存到本地,以避免任何并发问题。
### 属性Setters
只要绑定值有变化,生成的绑定类就会在视图中调用setter方法。数据绑定框架可以自定义要调用的方法来设置值。
### 自动Setters
对一个属性来说,数据绑定试图查找setAttribute方法。这与属性的命名空间没有关系,只与属性本身有关。
例如 TextView 的属性 android:text 上的表达式,数据绑定将查找setText(String) 方法,如果表达式返回值为 int,则会调用 setText(int)方法。注意表达式要返回正确的类型,有必要则使用cast进行类型转换。即使没有给定的属性,数据绑定也会执行。可以通过数据绑定调用setter方法创建属性。例如,DrawerLayout没有任何属性,但有很多setter方法,可以调用setter方法自动创建属性。
~~~
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
~~~
### 重命名Setters
对于属性setter方法与名字不匹配的情况,可以通过[BindingMethods](http://developer.android.com/reference/android/databinding/BindingMethods.html)注释关联名字和方法。类中包含[BindingMethod](http://developer.android.com/reference/android/databinding/BindingMethod.html)注释,其中可以重命名set方法。例如,android:tint 属性与[setImageTintList(ColorStateList)](#)方法相关,而与setTint不对应。
~~~
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
~~~
开发者不需要重命名setter方法,安卓框架已经做好了这方面的工作。
### 自定义 Setters
一些属性需要自定义绑定逻辑,android:paddingLeft 属性并没有对应的setter方法,但是存在setPadding(left, top, right, bottom)方法。通过 BindingAdapter 注释来自定义属性调用的静态setter方法。android 系统已经创建了 BindingAdapter 函数,下面是 paddingLeft 属性对应的函数:
~~~
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
~~~
绑定适配器对于其他自定义类型很有帮助。例如可以在其他线程中自定义加载图片。如果绑定适配器有冲突,则开发者自定义的将会重写系统默认的绑定适配方法。一个适配器还可以有多个参数:
~~~
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
<ImageView app:imageUrl=“@{venue.imageUrl}”
app:error=“@{@drawable/venueError}”/>
~~~
如果用于 ImageView 的 imageUrl和 error 参数都存在并且 imageUrl 是 string 类型、error 是 drawable 类型 则就会调用上面定义的适配器。
在匹配适配器的时候, 会忽略自定义的命名空间你也可以为 android 命名空间的属性自定义适配器。
~~~
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}
~~~
只能使用一个抽象方法的接口或抽象类调用事件处理程序。
~~~
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}
~~~
当一个监听器有多种方法时,必须要分割成多个监听器。
例如,OnAttachStateChangeListener包含两个方法, onViewAttachedToWindow() 和onViewDetachedFromWindow(),必须创建两个接口用于区分属性和处理程序。
~~~
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
~~~
一个监听器的改变也会影响另外一个,所以必须有三个不同的绑定适配器,每个监听器分别对应一个,两个监听器对应第三个绑定适配器。
~~~
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
~~~
上面的例子比正常的要稍微复杂,因为视图对监听器用添加和删除方法替代set方法。
android.databinding.adapters.ListenerUtil类帮助跟踪之前的监听器,以便在绑定适配器中及时移除相应的监听器。
通过用@TargetApi(VERSION_CODES.HONEYCOMB_MR1)注释接口OnViewDetachedFromWindow and OnViewAttachedToWindow 。数据绑定代码生成器知道只能在产生蜂窝MR1和新设备中运行时才会生成监听器。
### 转换器(Converters)
### 对象转换
当绑定表达式返回一个对象时候,将会自动调用 set 函数、重命名的函数、或者自定义的 setter 中的一个。表达式返回的对象将会转换为该函数的参数类型。
使用 ObservableMaps 来保存数据会比较简单。例如:
~~~
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
~~~
这里的 userMap 返回的对象将制动转换为 setText(CharSequence) 的参数。 如果参数不明确,则开发者需要强制转换为需要的类型。
### 自定义转换规则
有时候参数应该可以自动转换,例如
~~~
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
~~~
上面的背景需要一个 Drawable 对象,但是表达式的返回值为整数对象的颜色值。这种情况下,颜色值需要转换为 ColorDrawable。 这种转换通过一个静态函数完成,该函数带有一个 BindingConversion 注解。
~~~
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
~~~
需要注意的是,转换是在 setter上完成的,所以不能把不同的类型混合使用:
~~~
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
~~~
### 三、Android Studio 支持数据绑定
Android Studio支持数据绑定表达式的语法高亮显示,并在编辑器中显示任何表达式语言语法错误。
预览窗格显示数据绑定表达式的默认值。以下从布局XML文件的元素实例摘录。
~~~
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=PLACEHOLDER}"/>
~~~
如果项目的设计阶段需要显示一个默认值,可以使用工具属性而不是默认的表达式的值。
到此Data Binding官方文档就翻译完了。
Data Binding Guide——google官方文档翻译(上)
最后更新于:2022-04-01 14:26:16
android引入MVVM框架时间还不长,目前还很少有应用到app中的。但它是比较新的技术,使用它来搭建项目能省很多代码,而且能使用代码架构比较清晰。本篇文章是我在学习MVVM时翻译的,篇幅比较长,先翻译前半部分。
这篇文档解析如何使用数据绑定库来写响应式布局并减少用来绑定应用程序和布局之间冗余代码,使用逻辑层和布局分离。
数据绑定库提供了即灵活又全面的兼容性——它的支持库.so可以用在android2.1平台(API level 7+)。
使用MVVM需要Gradle1.5.0-alphal或更高版本的插件。
### 一、测试版
请注意,数据绑定库是一个测试版。虽然数据绑定是处于测试阶段,开发人员应该注意以下事项:
*目前它只是一个测试版,可能不适合你的用例,我们需要你的反馈。
*数据绑定库测试版有重大的改变,包括那些没有源代码与应用程序不兼容,也就是说,可能以后需要进行更改。
*开发人员应该随时发布应用程序构建与数据绑定库测试版,它与Android SDK和谷歌的服务条款适用,建议经常采用新库或工具来彻底测试自己的应用程序。
### 二、搭建环境
首先你需要在Android SDK manager中下载支持库。
配置您的应用程序,在你的module中的build.gradle文件中添加dataBinding元素。
使用下面的代码片段来配置数据绑定:
~~~
android {
....
dataBinding {
enabled = true
}
}
~~~
如果你的app使用到的库使用到数据绑定,那你的app也需要在build.gradle文件中进行配置。
另外,确保您正在使用一个兼容的版本的Android工作室。Android Studio 1.3或更新的版本支持数据绑定。
### 数据绑定的布局文件
### 编写你的第一个数据绑定布局
数据绑定布局文件略有不同,它以layout作为布局的起点,,后跟一个data标签和一个view元素。这个view元素是普通不使用数据绑定布局的根元素。一个示例文件是这样的:
~~~
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
~~~
data中使用的variable表示了一个可能会在这个布局中作用的属性。
~~~
<variable name="user" type="com.example.User"/>
~~~
布局属性的设置使用“@ { }”语法,这里TextView的文字属性就设置为user中的firstName属性。
~~~
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
~~~
#
### 对象
让我们假设现在有一个User的普通java对象(POJO):
~~~
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
~~~
这种类型的对象数据不会改变。通常在应用程序的数据读取一次,永远不会改变。它还可以改为javabean对象:
~~~
public class User {
private final String firstName;
private final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
~~~
从数据绑定的角度来看,这两个类是等价的。用于设置TextView的android:文本的表达式@{user.firstName}将访问第一个类中的firstName字段和后一个类的getFirstName()方法。另外,如果firstName()方法存在,它还将访问firstName()方法。
### 绑定数据
默认情况下,绑定类的名称是基于布局文件的名称起的,它是将布局文件名开头大写并加上“Binding”而成。上述布局文件名称为main_activity.xml,那它的绑定类名为MainActivityBinding。这个类拥有所有从属性(例如用户变量)到布局的绑定关系并知道如何赋值绑定表达式。最简单的方法创建绑定的方法就是通过反射:
~~~
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
~~~
完成了!运行应用程序,你会看到在UI上看到“Test User”。另外,你也可以通过以下代码来获取view:
~~~
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
~~~
如果你想在 ListView或者RecyclerView 使用数据绑定,你应该这样使用:
~~~
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
~~~
绑定事件
事件可以直接绑定到处理程序方法,类似于android:onClick可以分配给一个Activity的方法。事件属性名称是listener方法的名称有一部分。例如,[View.OnLongClickListener](http://developer.android.com/reference/android/view/View.OnLongClickListener.html)有一个onLongClick()方法,所以这个事件的属性名应写为android:onLongClick。
分配一个事件给handler,使用方法名称来作为正常绑定表达式的变量,例如,如果您的数据对象有两个方法:
~~~
public class MyHandlers {
public void onClickFriend(View view) { ... }
public void onClickEnemy(View view) { ... }
}
~~~
绑定表达式会为View 分配一个click监听事件。
~~~
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.Handlers"/>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
</LinearLayout>
</layout>
~~~
### 三、布局细节
### Imports
零个或多个导入元素可以使用内部数据元素。这些允许简单引用类内部布局文件,就像在Java。
~~~
<data>
<import type="android.view.View"/>
</data>
~~~
现在,view可以使用在你的绑定表达式:
~~~
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
~~~
当有类名称冲突,其中一个类可能用alias进行重命名:
~~~
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
~~~
现在,Vista可以使用 com.example.real.estate.view中的引用并且View可用于代表android.view.view。导入类型可以在变量和表达式中作为类型引用:
~~~
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
~~~
注意:Android studio还没处理好import的自动导入,因此你的IDE可能无法自动导入变量,你可以在变量定义使用完全限定名称,这样你的应用程序仍可以正常编译。
~~~
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
~~~
导入类型时也可以使用在引用静态字段和方法中:
~~~
<data>
<import type="com.example.MyStringUtils"/>
<variable name="user" type="com.example.User"/>
</data>
…
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
~~~
就像在java中,java.lang.*会自动导入一样。
### 变量
数据元素可以使用任意数量的变量元素。每个变量元素描述一个属性,它都必须在layout中通过绑定表达式来设置值。
~~~
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
~~~
变量在编译时进行类型检查,如果一个变量实现了Observable接口或者它是一个observable集合,那应该反射得到。如果变量是一个没有实现Observable的基类或接口,,那变量将无法被观察到!
当不同的布局文件中的变量配置不相同(如landscape或者protrait),这些变量将被组合起来。在这些布局文件之间定义变量不会有冲突。
生成的绑定类中,对应每个变量都有一个setter和getter方法。变量将设置为默认的Java值直到setter被调用,如引用类型默认为null,int默认值是0,boolean默认为false等。
### Custom Binding Class Names
默认情况下,生成绑定类名称是基于布局文件的名称的,把布局文件名称开头用大写,删除下划线(_),最后加上”Binding”。这个类将被放置在module的一个databinding包模块下。例如,contact_item.xml将产生一个类名ContactItemBinding的类。如果module包名为com.example.my。那么它将会被放置在com.example.my.app.databinding这个包下。
通过调整数据元素的class属性可以将绑定类重命名或放置在不同的包。例如:
~~~
<data class="ContactItem">
...
</data>
~~~
这个生成绑定类ContactItem将会存在module包下的databinding包中。如果想让生成的类存放在module中的另一个包下,它加入前缀”.”:
~~~
<data class=".ContactItem">
...
</data>
~~~
这种情况下,ContactItem将被直接放置在module包下,如果想放在其它包可以使用包名的全称:
~~~
<data class="com.example.ContactItem">
...
</data>
~~~
### Includes
变量可以通过一个included元素导入到布局文件中,included中要有应用程序名称空间和属性的变量名:
~~~
?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>
~~~
### 在这里,name.xml和contact.xml中都必须要有user变量。
数据绑定不支持直接在merge元素里添加include并以此来作为子view,
~~~
<pre name="code" class="html"><span style="font-weight: normal;"><?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<merge>
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout></span>
~~~
### 表达式语法
### 共同特性
表达式语法跟java语法很像,下面的一样的语法:
· 数学计算 + - / * %
· 字符串连接 +
· 逻辑运算符&& ||
· 位运算& | ^
· 一元运算 + - ! ~
· 位移>> >>> <<
· 比较== > < >= <=
· instanceof
· Grouping ()
· 文字 - character, String, numeric, null
· Cast
· 方法调用
· 字段访问
· 数组访问 [ ]
· 三元运算符?:
例如:
~~~
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
~~~
### 不具备的方法
有一些操作只能在Java表达式中使用。
· this
· super
· new
· Explicit generic invocation
#### 空联合操作
联合操作符(??)不为空的话选择左边操作,为空的话选择右边操作。
~~~
android:text="@{user.displayName ?? user.lastName}"
~~~
它等同于:
~~~
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
~~~
#### 避免指针异常
生成数据绑定代码会自动检查null,避免空指针异常。例如,在表达式@ { user.name },如果用户是null,user.name分配其默认值(null)。如果是@ { user.age }。age是int,那么它将默认值为0。
#### Collections
集合共同点:数组,list,sparse list和map,为了方便访问,均可以使用[ ]操作符。
~~~
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
~~~
### 文字字符串
使用单引号包裹住属性值,这样就很容易在表达式中使用双引号:
~~~
android:text='@{map["firstName"]}'
~~~
还可以使用双引号包围的属性值。当这样做时,字符串应该使用"或引号(`)。
~~~
android:text="@{map[`firstName`}"
android:text="@{map["firstName"]}"
~~~
### 资源文件
可以使用正常的访问资源的表达式语法:
~~~
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
~~~
格式字符串和复数可以通过提供参数来定义:
~~~
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
~~~
当一个复数多个参数,所有参数都应该通过:
~~~
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
~~~
一些资源需要显式类型的评估:.
<table class=" "><tbody><tr><td valign="top" style="background:rgb(153,153,153)"><p><span style="color:rgb(255,255,255)">Type</span></p></td><td valign="top" style="background:rgb(153,153,153)"><p><span style="color:rgb(255,255,255)">Normal Reference</span></p></td><td valign="top" style="background:rgb(153,153,153)"><p><span style="color:rgb(255,255,255)">Expression Reference</span></p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>String[]</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@array</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@stringArray</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>int[]</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@array</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@intArray</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>TypedArray</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@array</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@typedArray</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>Animator</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@animator</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@animator</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>StateListAnimator</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@animator</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@stateListAnimator</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>color <span style="color:rgb(0,102,0)">int</span></p></td><td valign="top" style="background:rgb(247,247,247)"><p>@color</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@color</p></td></tr><tr><td valign="top" style="background:rgb(247,247,247)"><p>ColorStateList</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@color</p></td><td valign="top" style="background:rgb(247,247,247)"><p>@colorStateList</p></td></tr></tbody></table>
Activity启动过程源码分析
最后更新于:2022-04-01 14:26:14
其实写分析源码文章总会显得很复杂很乏味,但是梳理自己看源码时的一些总结也是一种提高。这篇博客分析下Activity启动过程源码,我会尽量说得简单点。个人的观点是看源码不能看得太细,否则就会花费很多时间并很难理清整个过程。所以本次分析重在理清activity的一个启动流程。
首先大概总结下activity启动的整个流程,这个过程涉及到Instrumentation,ActivityThread,和ActivityManagerService(AMS)。通过Binder向AMS发请求,AMS内部有一个ActivityStack,它负责栈内的Activity同步,AMS去通过ActivityThread去调用Activity的生命周期方法完成Activity的启动。如果对Binder进程间通信不了解可看下[IPC——android进程间通信](http://blog.csdn.net/u014486880/article/details/50354017)
先上一张总图,等看完博客可以再回头来看下这图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b6926f5.jpg)
### 一.启动预处理
启动一个activity最常用的方法就是startActivityForResult或者startActivity,而startActivity也是调用startActivityForResult,所以此次分析入口当然是startActivityForResult。
~~~
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
if (mParent == null) {
Instrumentation.ActivityResult ar =
mInstrumentation .execStartActivity(
this, mMainThread .getApplicationThread() , mToken, this,
intent, requestCode, options) ;
if (ar != null) {
mMainThread .sendActivityResult(
mToken, mEmbeddedID, requestCode , ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
// If this start is requesting a result, we can avoid making
// the activity visible until the result is received. Setting
// this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
// activity hidden during this time, to avoid flickering.
// This can only be done when a result is requested because
// that guarantees we will get information back when the
// activity is finished, no matter what happens to it.
mStartedActivity = true;
}
final View decor = mWindow != null ? mWindow .peekDecorView() : null;
if (decor != null) {
decor.cancelPendingInputEvents();
}
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if (options != null) {
mParent .startActivityFromChild( this, intent, requestCode, options) ;
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent .startActivityFromChild( this, intent, requestCode);
}
}
if (options != null && !isTopOfTask()) {
mActivityTransitionState .startExitOutTransition( this, options);
}
}
~~~
Intrumentation它用来监控应用程序和系统的交互。而mMainThread.getApplicationThread()获取ApplicationThread,它是ActivityThread的一个内部类,是一个Binder对象,后面我们会看到,ActivityManagerService会使用它来和ActivityThread来进行进程间通信。上面的代码最终调用了execStartActivity方法。
~~~
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token , Activity target,
Intent intent , int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
if ( mActivityMonitors != null ) {
synchronized (mSync) {
final int N = mActivityMonitors.size() ;
for ( int i=0 ; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
if (am.match(who, null, intent)) {
am. mHits++;
if (am.isBlocking()) {
return requestCode >= 0 ? am.getResult() : null;
}
break;
}
}
}
}
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess() ;
int result = ActivityManagerNative. getDefault()
.startActivity(whoThread, who.getBasePackageName() , intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options) ;
checkStartActivityResult(result, intent) ;
} catch (RemoteException e) {
}
return null;
}
~~~
这里的intent.resolveTypeIfNeeded返回这个intent的MIME类型,如果没有在AndroidManifest.xml设置MainActivity的MIME类型那就返回null。启动的真正实现类由ActivityManagerNative.getDefault()的startActivity方法完成。先分析下ActivityManagerNative.getDefault()。
~~~
public abstract class ActivityManagerNative extends Binder implements IActivityManager{
…………
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity") ;
if ( false) {
Log.v ("ActivityManager" , "default service binder = " + b);
}
IActivityManager am = asInterface (b);
if ( false) {
Log.v ("ActivityManager" , "default service = " + am);
}
return am;
}
};
}
~~~
可以看到ActivityManagerNative是一个抽象类,它继承Binder,并实现了IActivityManager接口,ActivityManagerService(下称AMS)继承着ActivityManagerNative,所以它是IActivityManager的具体实现类。ActivityManagerNative.getDefault是一个IActivityManager类型的Binder对象,因此它的具体实现是AMS。获取的AMS的Binder对象是一个单例。ActivityManagerNative就相当于AIDL文件自动生成的那个类。ActivityManagerProxy是ActivityManagerNative中的一个代理方法,看下它的startActivity代码:
~~~
class ActivityManagerProxy implements IActivityManager
{
public ActivityManagerProxy(IBinder remote)
{
mRemote = remote;
}
public IBinder asBinder()
{
return mRemote;
}
public int startActivity(IApplicationThread caller, String callingPackage, Intent intent,
String resolvedType, IBinder resultTo, String resultWho, int requestCode,
int startFlags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IActivityManager.descriptor);
data.writeStrongBinder(caller != null ? caller.asBinder() : null);
data.writeString(callingPackage);
intent.writeToParcel(data, 0);
data.writeString(resolvedType);
data.writeStrongBinder(resultTo);
data.writeString(resultWho);
data.writeInt(requestCode);
data.writeInt(startFlags);
if (profilerInfo != null) {
data.writeInt(1);
profilerInfo.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
data.writeInt(0);
}
if (options != null) {
data.writeInt(1);
options.writeToParcel(data, 0);
} else {
data.writeInt(0);
}
mRemote.transact(START_ACTIVITY_TRANSACTION, data, reply, 0);
reply.readException();
int result = reply.readInt();
reply.recycle();
data.recycle();
return result;
}
public int startActivityAsUser(IApplicationThread caller, String callingPackage, Intent intent,
String resolvedType, IBinder resultTo, String resultWho, int requestCode,
int startFlags, ProfilerInfo profilerInfo, Bundle options,
int userId) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IActivityManager.descriptor);
data.writeStrongBinder(caller != null ? caller.asBinder() : null);
data.writeString(callingPackage);
intent.writeToParcel(data, 0);
data.writeString(resolvedType);
data.writeStrongBinder(resultTo);
data.writeString(resultWho);
data.writeInt(requestCode);
data.writeInt(startFlags);
if (profilerInfo != null) {
data.writeInt(1);
profilerInfo.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
} else {
data.writeInt(0);
}
if (options != null) {
data.writeInt(1);
options.writeToParcel(data, 0);
} else {
data.writeInt(0);
}
data.writeInt(userId);
mRemote.transact(START_ACTIVITY_AS_USER_TRANSACTION, data, reply, 0);
reply.readException();
int result = reply.readInt();
reply.recycle();
data.recycle();
return result;
…………
}
~~~
看到这就会发现,其实就是AIDL来进行进程间通信。它是真正实现类还应该是AMS中的startActivity。
~~~
public int startActivity(IBinder whoThread, String callingPackage,
Intent intent, String resolvedType, Bundle options) {
checkCaller();
int callingUser = UserHandle.getCallingUserId();
TaskRecord tr;
IApplicationThread appThread;
synchronized (ActivityManagerService.this) {
tr = recentTaskForIdLocked(mTaskId);
if (tr == null) {
throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
}
appThread = ApplicationThreadNative.asInterface(whoThread);
if (appThread == null) {
throw new IllegalArgumentException("Bad app thread " + appThread);
}
}
return mStackSupervisor.startActivityMayWait(appThread, -1, callingPackage, intent,
resolvedType, null, null, null, null, 0, 0, null, null,
null, options, callingUser, null, tr);
}
~~~
最后调用了 mStackSupervisor.startActivityMayWait,主要看两部分:
1.解析Intent。
下面语句对参数intent的内容进行解析,得到MainActivity的相关信息,保存在aInfo变量中:
~~~
ActivityInfo aInfo;
try {
ResolveInfo rInfo =
AppGlobals.getPackageManager().resolveIntent(
intent, resolvedType,
PackageManager.MATCH_DEFAULT_ONLY
| ActivityManagerService.STOCK_PM_FLAGS);
aInfo = rInfo != null ? rInfo.activityInfo : null;
} catch (RemoteException e) {
......
}
~~~
2.:调用startActivityLocked。
~~~
int res = startActivityLocked(caller, intent, resolvedType, aInfo,
voiceSession, voiceInteractor, resultTo, resultWho,
requestCode, callingPid, callingUid, callingPackage,
realCallingPid, realCallingUid, startFlags, options,
componentSpecified, null, container, inTask);
~~~
最的返回了startActivityLocked,它在ActivityStackSupervisor中,再看下它的代码:
~~~
final int startActivityLocked(IApplicationThread caller,
Intent intent, String resolvedType, ActivityInfo aInfo,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode,
int callingPid, int callingUid, String callingPackage,
int realCallingPid, int realCallingUid, int startFlags, Bundle options,
boolean componentSpecified, ActivityRecord[] outActivity, ActivityContainer container,
TaskRecord inTask) {
int err = ActivityManager.START_SUCCESS;
ProcessRecord callerApp = null;
if (caller != null) {
callerApp = mService.getRecordForAppLocked(caller);
if (callerApp != null) {
callingPid = callerApp.pid;
callingUid = callerApp.info.uid;
} else {
Slog.w(TAG, "Unable to find app for caller " + caller
+ " (pid=" + callingPid + ") when starting: "
+ intent.toString());
err = ActivityManager.START_PERMISSION_DENIED;
}
}
…………
ActivityRecord sourceRecord = null;
ActivityRecord resultRecord = null;
if (resultTo != null) {
sourceRecord = isInAnyStackLocked(resultTo);
if (DEBUG_RESULTS) Slog.v(
TAG, "Will send result to " + resultTo + " " + sourceRecord);
if (sourceRecord != null) {
if (requestCode >= 0 && !sourceRecord.finishing) {
resultRecord = sourceRecord;
}
}
}
final int launchFlags = intent.getFlags();
if ((launchFlags&Intent.FLAG_ACTIVITY_FORWARD_RESULT) != 0 && sourceRecord != null) {
// Transfer the result target from the source activity to the new
// one being started, including any failures.
if (requestCode >= 0) {
ActivityOptions.abort(options);
return ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT;
}
resultRecord = sourceRecord.resultTo;
resultWho = sourceRecord.resultWho;
requestCode = sourceRecord.requestCode;
sourceRecord.resultTo = null;
…………
ActivityRecord r = new ActivityRecord(mService, callerApp, callingUid, callingPackage,
intent, resolvedType, aInfo, mService.mConfiguration, resultRecord, resultWho,
requestCode, componentSpecified, this, container, options);
^………………
err = startActivityUncheckedLocked(r, sourceRecord, voiceSession, voiceInteractor,
startFlags, true, options, inTask);
~~~
分三步:
1.从传进来的参数caller得到调用者的进程信息,并保存在callerApp变量中,这里就是Launcher应用程序的进程信息了。 前面说过,参数resultTo是Launcher这个Activity里面的一个Binder对象,通过它可以获得Launcher这个Activity的相关信息,保存在sourceRecord变量中。
2.创建即将要启动的Activity的相关信息,并保存在r变量中。
3.调用startActivityUncheckedLocked。
startActivityUncheckedLocked的核心代码如下:
~~~
final boolean launchSingleTop = r.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP;
final boolean launchSingleInstance = r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE;
final boolean launchSingleTask = r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK;
int launchFlags = intent.getFlags();
if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0 &&
(launchSingleInstance || launchSingleTask)) {
// We have a conflict between the Intent and the Activity manifest, manifest wins.
Slog.i(TAG, "Ignoring FLAG_ACTIVITY_NEW_DOCUMENT, launchMode is " +
"\"singleInstance\" or \"singleTask\"");
launchFlags &=
~(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
…………
if (doResume) {
resumeTopActivitiesLocked();
}
ActivityOptions.abort(options);
~~~
函数首先获得intent的标志值,保存在launchFlags变量中。。Activity的启动方式有四种,接下来一系列判断就是决定Activity四种不同启动模式的启动方式。然后判断当前要启动的Activity是否就是当前堆栈顶端的Activity,如果是的话,在某些情况下,就不用再重新启动了。最后调用了resumeTopActivitiesLocked,现在这个过程己经从ActivityStackSupervisor转移到ActivityStack。
~~~
final boolean resumeTopActivityLocked(ActivityRecord prev, Bundle options) {
if (mStackSupervisor.inResumeTopActivity) {
// Don't even start recursing.
return false;
}
boolean result = false;
try {
// Protect against recursion.
mStackSupervisor.inResumeTopActivity = true;
if (mService.mLockScreenShown == ActivityManagerService.LOCK_SCREEN_LEAVING) {
mService.mLockScreenShown = ActivityManagerService.LOCK_SCREEN_HIDDEN;
mService.updateSleepIfNeededLocked();
}
result = resumeTopActivityInnerLocked(prev, options);
} finally {
mStackSupervisor.inResumeTopActivity = false;
}
return result;
}
~~~
通过上面的代码可知,resumeTopActivitiesLocked调用了resumeTopActivityInnerLocked方法。来看下resumeTopActivityInnerLocked源代码,由于这部分代码很长,只贴出它的最主要流程:
~~~
if ((mService.mSleeping || mService.mShuttingDown)
<span style="white-space:pre"> </span> && mLastPausedActivity == next && next.state == ActivityState.PAUSED) {
<span style="white-space:pre"> </span> ......
<span style="white-space:pre"> </span> return false;
<span style="white-space:pre"> </span> }
.......
<span style="white-space:pre"> </span> // We need to start pausing the current activity so the top one
<span style="white-space:pre"> </span> // can be resumed...
<span style="white-space:pre"> </span> if (mResumedActivity != null) {
<span style="white-space:pre"> </span> if (mResumedActivity != null) {
if (DEBUG_STATES) Slog.d(TAG, "resumeTopActivityLocked: Pausing " + mResumedActivity);
pausing |= startPausingLocked(userLeaving, false, true, dontWaitForPause);
}
<span style="white-space:pre"> </span> return true;
<span style="white-space:pre"> </span> }
<span style="white-space:pre"> </span> ......
<span style="white-space:pre"> </span> if (next.app != null && next.app.thread != null) {
<span style="white-space:pre"> </span> ......
<span style="white-space:pre"> </span> } else {
<span style="white-space:pre"> </span> ......
<span style="white-space:pre"> </span> startSpecificActivityLocked(next, true, true);
<span style="white-space:pre"> </span> }
<span style="white-space:pre"> </span> return true;
<span style="white-space:pre"> </span> }
~~~
由上可以清晰的看到,如果当前activity没暂停,要先把它暂停。
### 二.暂停当前Activity
上面己经分析到新Activity启动会先把之前的Activity暂停,具体是调用startPausingLocked方法。它的核心代码如下:
~~~
if (prev.app != null && prev.app.thread != null) {
if (DEBUG_PAUSE) Slog.v(TAG, "Enqueueing pending pause: " + prev);
try {
EventLog.writeEvent(EventLogTags.AM_PAUSE_ACTIVITY,
prev.userId, System.identityHashCode(prev),
prev.shortComponentName);
mService.updateUsageStats(prev, false);
prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing,
userLeaving, prev.configChangeFlags, dontWait);
~~~
prev.app.thread是一个ApplicationThread对象的远程接口,它的类型是IApplicationThread。通过调用这个远程接口的schedulePauseActivity来通知Activity进入Paused状态。先看下IApplicationThread接口。
~~~
public interface IApplicationThread extends IInterface {
void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, boolean dontReport) throws RemoteException;
void scheduleStopActivity(IBinder token, boolean showWindow,
int configChanges) throws RemoteException;
void scheduleWindowVisibility(IBinder token, boolean showWindow) throws RemoteException;
void scheduleSleeping(IBinder token, boolean sleeping) throws RemoteException;
void scheduleResumeActivity(IBinder token, int procState, boolean isForward, Bundle resumeArgs)
throws RemoteException;
void scheduleSendResult(IBinder token, List<ResultInfo> results) throws RemoteException;
void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo,
String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state,
PersistableBundle persistentState, List<ResultInfo> pendingResults,
List<ReferrerIntent> pendingNewIntents, boolean notResumed, boolean isForward,
ProfilerInfo profilerInfo) throws RemoteException;
void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults,
List<ReferrerIntent> pendingNewIntents, int configChanges,
boolean notResumed, Configuration config) throws RemoteException;
void scheduleNewIntent(List<ReferrerIntent> intent, IBinder token) throws RemoteException;
void scheduleDestroyActivity(IBinder token, boolean finished,
int configChanges) throws RemoteException;
void scheduleReceiver(Intent intent, ActivityInfo info, CompatibilityInfo compatInfo,
int resultCode, String data, Bundle extras, boolean sync,
int sendingUser, int processState) throws RemoteException;
static final int BACKUP_MODE_INCREMENTAL = 0;
static final int BACKUP_MODE_FULL = 1;
static final int BACKUP_MODE_RESTORE = 2;
static final int BACKUP_MODE_RESTORE_FULL = 3;
void scheduleCreateBackupAgent(ApplicationInfo app, CompatibilityInfo compatInfo,
int backupMode) throws RemoteException;
void scheduleDestroyBackupAgent(ApplicationInfo app, CompatibilityInfo compatInfo)
throws RemoteException;
void scheduleCreateService(IBinder token, ServiceInfo info,
CompatibilityInfo compatInfo, int processState) throws RemoteException;
void scheduleBindService(IBinder token,
Intent intent, boolean rebind, int processState) throws RemoteException;
void scheduleUnbindService(IBinder token,
Intent intent) throws RemoteException;
void scheduleServiceArgs(IBinder token, boolean taskRemoved, int startId,
int flags, Intent args) throws RemoteException;
void scheduleStopService(IBinder token) throws RemoteException;
static final int DEBUG_OFF = 0;
static final int DEBUG_ON = 1;
static final int DEBUG_WAIT = 2;
void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers,
ComponentName testName, ProfilerInfo profilerInfo, Bundle testArguments,
IInstrumentationWatcher testWatcher, IUiAutomationConnection uiAutomationConnection,
int debugMode, boolean openGlTrace, boolean restrictedBackupMode, boolean persistent,
Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services,
Bundle coreSettings) throws RemoteException;
void scheduleExit() throws RemoteException;
void scheduleSuicide() throws RemoteException;
void scheduleConfigurationChanged(Configuration config) throws RemoteException;
void updateTimeZone() throws RemoteException;
void clearDnsCache() throws RemoteException;
void setHttpProxy(String proxy, String port, String exclList,
Uri pacFileUrl) throws RemoteException;
void processInBackground() throws RemoteException;
void dumpService(FileDescriptor fd, IBinder servicetoken, String[] args)
throws RemoteException;
void dumpProvider(FileDescriptor fd, IBinder servicetoken, String[] args)
throws RemoteException;
void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
int resultCode, String data, Bundle extras, boolean ordered,
boolean sticky, int sendingUser, int processState) throws RemoteException;
void scheduleLowMemory() throws RemoteException;
void scheduleActivityConfigurationChanged(IBinder token) throws RemoteException;
void profilerControl(boolean start, ProfilerInfo profilerInfo, int profileType)
throws RemoteException;
void dumpHeap(boolean managed, String path, ParcelFileDescriptor fd)
throws RemoteException;
~~~
由此猜测它与activity,service的开启有关。IApplicationThread这个IBind实现者完成了大量和Activity以及service有关的功能。
而ApplicationThreadNative就是它的实现类。
~~~
public abstract class ApplicationThreadNative extends Binder
implements IApplicationThread {
/**
* Cast a Binder object into an application thread interface, generating
* a proxy if needed.
*/
static public IApplicationThread asInterface(IBinder obj) {
if (obj == null) {
return null;
}
IApplicationThread in =
(IApplicationThread)obj.queryLocalInterface(descriptor);
if (in != null) {
return in;
}
return new ApplicationThreadProxy(obj);
}
public ApplicationThreadNative() {
attachInterface(this, descriptor);
}
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
switch (code) {
case SCHEDULE_PAUSE_ACTIVITY_TRANSACTION:
{
data.enforceInterface(IApplicationThread.descriptor);
IBinder b = data.readStrongBinder();
boolean finished = data.readInt() != 0;
boolean userLeaving = data.readInt() != 0;
int configChanges = data.readInt();
boolean dontReport = data.readInt() != 0;
schedulePauseActivity(b, finished, userLeaving, configChanges, dontReport);
return true;
}
~~~
~~~
<pre name="code" class="java" style="color: rgb(51, 51, 51); line-height: 26px; orphans: 2; widows: 2;">public final void schedulePauseActivity(IBinder token, boolean finished,
boolean userLeaving, int configChanges, boolean dontReport) throws RemoteException {
Parcel data = Parcel.obtain();
data.writeInterfaceToken(IApplicationThread.descriptor);
data.writeStrongBinder(token);
data.writeInt(finished ? 1 : 0);
data.writeInt(userLeaving ? 1 :0);
data.writeInt(configChanges);
data.writeInt(dontReport ? 1 : 0);
mRemote.transact(SCHEDULE_PAUSE_ACTIVITY_TRANSACTION, data, null,
IBinder.FLAG_ONEWAY);
data.recycle();
}
………………}
~~~
由上可见它就是一个Binder抽象类,ApplicationThreadProxy是代理类。真正实现类是ApplicationThread。直接看ApplicationThread的schedulePauseActivity方法。
~~~
public final void schedulePauseActivity(IBinder token, boolean finished,
boolean userLeaving, int configChanges, boolean dontReport) {
sendMessage(
finished ? H.PAUSE_ACTIVITY_FINISHING : H.PAUSE_ACTIVITY,
token,
(userLeaving ? 1 : 0) | (dontReport ? 2 : 0),
configChanges);
}
~~~
看到这,就知道接下来肯定是用Handler来发送消息了,发送消息的代码如下:
~~~
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
if (DEBUG_MESSAGES) Slog.v(
TAG, "SCHEDULE " + what + " " + mH.codeToString(what)
+ ": " + arg1 + " / " + obj);
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
if (async) {
msg.setAsynchronous(true);
}
mH.sendMessage(msg);
}
~~~
就是在发送一个暂停Activity的消息给Handler处理,这个Handler名字为H。H在它的handlerMessage中处理相应的请求,它的实现如下:
~~~
public void handleMessage(Message msg) {
switch (msg.what) {
…………
case PAUSE_ACTIVITY:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
handlePauseActivity((IBinder)msg.obj, false, (msg.arg1&1) != 0, msg.arg2,
(msg.arg1&2) != 0);
maybeSnapshot();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
…………
}
~~~
看下handlePauseActivity的代码:
~~~
private void handlePauseActivity(IBinder token, boolean finished,
boolean userLeaving, int configChanges, boolean dontReport) {
ActivityClientRecord r = mActivities.get(token);
if (r != null) {
//Slog.v(TAG, "userLeaving=" + userLeaving + " handling pause of " + r);
if (userLeaving) {
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(token, finished, r.isPreHoneycomb());
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
// Tell the activity manager we have paused.
if (!dontReport) {
try {
ActivityManagerNative.getDefault().activityPaused(token);
} catch (RemoteException ex) {
}
}
mSomeActivitiesChanged = true;
}
~~~
函数首先将Binder引用token转换成ActivityRecord的远程接口ActivityClientRecord,然后做了三个事情:1. 如果userLeaving为true,则通过调用performUserLeavingActivity函数来调用Activity.onUserLeaveHint通知Activity,用户要离开它了;2. 调用performPauseActivity函数来调用Activity.onPause函数,我们知道,在Activity的生命周期中,当它要让位于其它的Activity时,系统就会调用它的onPause函数;3. 它通知ActivityManagerService,这个Activity已经进入Paused状态了,ActivityManagerService现在可以完成未竟的事情,即启动MainActivity了。
看下ActivityManagerService.activityPaused的代码:
~~~
public final void activityPaused(IBinder token) {
final long origId = Binder.clearCallingIdentity();
synchronized(this) {
ActivityStack stack = ActivityRecord.getStackLocked(token);
if (stack != null) {
stack.activityPausedLocked(token, false);
}
}
Binder.restoreCallingIdentity(origId);
}
~~~
绕了一大圈又回到了ActivityStack中,调用它的activityPauseLocked方法。
~~~
final void activityPausedLocked(IBinder token, boolean timeout) {
if (DEBUG_PAUSE) Slog.v(
TAG, "Activity paused: token=" + token + ", timeout=" + timeout);
final ActivityRecord r = isInStackLocked(token);
if (r != null) {
mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r);
if (mPausingActivity == r) {
if (DEBUG_STATES) Slog.v(TAG, "Moving to PAUSED: " + r
+ (timeout ? " (due to timeout)" : " (pause complete)"));
completePauseLocked(true);
} else {
EventLog.writeEvent(EventLogTags.AM_FAILED_TO_PAUSE,
r.userId, System.identityHashCode(r), r.shortComponentName,
mPausingActivity != null
? mPausingActivity.shortComponentName : "(none)");
}
}
}
~~~
前一个Activity的信息保存在mPausingActivity中,因此,这里mPausingActivity等于r,于是,执行completePauseLocked操作。
~~~
private void completePauseLocked(boolean resumeNext) {
…………
if (resumeNext) {
final ActivityStack topStack = mStackSupervisor.getFocusedStack();
if (!mService.isSleepingOrShuttingDown()) {
mStackSupervisor.resumeTopActivitiesLocked(topStack, prev, null);
} else {
mStackSupervisor.checkReadyForSleepLocked();
ActivityRecord top = topStack.topRunningActivityLocked(null);
if (top == null || (prev != null && top != prev)) {
// If there are no more activities available to run,
// do resume anyway to start something. Also if the top
// activity on the stack is not the just paused activity,
// we need to go ahead and resume it to ensure we complete
// an in-flight app switch.
mStackSupervisor.resumeTopActivitiesLocked(topStack, null, null);
}
}
}
…………
}
~~~
很显然,又回到了resumeTopActivitiesLocked中,这次activity己经停止,所以它调用了ActivityStackSupervisor的StartSpecificActivityLocked方法。
### 三,启动Activity
其实启动Activity与暂停很类似,都是在ApplicationThread中实现的,看代码:
~~~
void startSpecificActivityLocked(ActivityRecord r,
boolean andResume, boolean checkConfig) {
// Is this activity's application already running?
ProcessRecord app = mService.getProcessRecordLocked(r.processName,
r.info.applicationInfo.uid, true);
r.task.stack.setLaunchTime(r);
if (app != null && app.thread != null) {
try {
if ((r.info.flags&ActivityInfo.FLAG_MULTIPROCESS) == 0
|| !"android".equals(r.info.packageName)) {
// Don't add this if it is a platform component that is marked
// to run in multiple processes, because this is actually
// part of the framework so doesn't make sense to track as a
// separate apk in the process.
app.addPackage(r.info.packageName, r.info.applicationInfo.versionCode,
mService.mProcessStats);
}
realStartActivityLocked(r, app, andResume, checkConfig);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Exception when starting activity "
+ r.intent.getComponent().flattenToShortString(), e);
}
// If a dead object exception was thrown -- fall through to
// restart the application.
}
mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
"activity", r.intent.getComponent(), false, false, true);
}
~~~
接下来重点看下realStartActivityLocked,它代码中有以下一段
~~~
app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken,
System.identityHashCode(r), r.info, new Configuration(mService.mConfiguration),
r.compat, r.launchedFromPackage, r.task.voiceInteractor, app.repProcState,
r.icicle, r.persistentState, results, newIntents, !andResume,
mService.isNextTransitionForward(), profilerInfo);
~~~
是不是有种似曾相识的感觉。接下来就是跟暂停一样了,调用ApplicationThread中的scheduleLaunchActivity,最终调用H中的HandlerMessage。
~~~
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
………………
~~~
再看下handleLaunchActivity()的实现,代码比较长,只看核心的。
~~~
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
// Initialize before creating the activity
WindowManagerGlobal.initialize();
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
~~~
performLaunchAcitvity中调用Activity的onCreate(),onStart()方法,handlerResumeActivity调用onResume()方法。重点看下performLaunchAcitvity,我们把它拆分来看,它总共完成以下几个功能。
1.从ActivityClientRecord中获取待启动的Activity的组件信息
~~~
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
ComponentName component = r.intent.getComponent();
if (component == null) {
component = r.intent.resolveActivity(
mInitialApplication.getPackageManager());
r.intent.setComponent(component);
}
if (r.activityInfo.targetActivity != null) {
component = new ComponentName(r.activityInfo.packageName,
r.activityInfo.targetActivity);
}
~~~
2.通过Instrumentation 的newActivity方法使用类加载器创建Activity对象
~~~
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
~~~
重点实现在newActivity中
~~~
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
~~~
3.通过LoadedApk的makeApplication方法来尝试创建Application
~~~
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
mActivityThread.mAllApplications.add(app);
mApplication = app;
if (instrumentation != null) {
try {
instrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!instrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}
…………
~~~
如果Application被创建了,就不会再重复创建了。这也意味着一个应用只有一个Application,它的创建是通过Instrumentation来完成的。通过类加载载来实现。创建完通过callApplicationOnCreate来调用Application的onCreate()方法。
4,创建ContextImpl对象并通过Activity的attach方法来完成一些重要数据的初始化。
~~~
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
~~~
ContextImpl是通过Activity的attach方法与Acitivity建立关联的。此外attach来完成window的创建并建立自己和window的关联。这样当window接收到外部输入事件后就可以将事件传递给Activity。
到些Activity就启动起来了。
### 四,总结
在应用程序内部启动新的Activity的过程要执行很多步骤,但是整体来看,主要分为以下四个阶段:
1. 应用程序的MainActivity通过Binder进程间通信机制通知ActivityManagerService,它要启动一个新的Activity;
2. :ActivityManagerService通过Binder进程间通信机制通知MainActivity进入Paused状态;
3. MainActivity通过Binder进程间通信机制通知ActivityManagerService,它已经准备就绪进入Paused状态,于是ActivityManagerService就准备要在MainActivity所在的进程和任务中启动新的Activity了;
4. ActivityManagerService通过Binder进程间通信机制通知MainActivity所在的ActivityThread,现在一切准备就绪,它可以真正执行Activity的启动操作了。
现在再回头去看下那个启动图,应该大部分能看懂了。
Activity你需要知道的一切
最后更新于:2022-04-01 14:26:11
最近想写篇关于Activity启动过程源码分析的博客,在此之前先总结下Android中Activity必须要知道的一些基础知识,以方便后面能看懂Activity的源码。
### 一,Activity生命周期和启动模式
activity最经典的启动模式图如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b66a3fc.jpg)
它分为onCreate--onStart--onResume--onPause--onStop--onDestory.这几个阶段,这个是个android开发者肯定都很熟悉,这里不详细说,只是说下一些注意事项:
1.onStart和onStop的区别:onStart表示应用己经可见,但只运行在后台,没到前台。onStop表示acitvity运行在后台。当用户使用透明主题,不会调用onstop.
2.onResume和onpause的区别:这两个是从activity是否位于前台来回调的,是一组。
3.当新Activity启动之前,栈顶acitivity需要先onPause新的再启动。所以不能在onpause中做耗时操作。
4.当activity异常停止时,会调用onSaveInstanceState,并把所保存的Bundle传递给onRestoreInstanceState,onRestoreInstanceState是在onStart之后被调用。过程是onSaveInstanceState先保存数据,Activity会委托Window保存数据,接着Window再委托上面的顶级容器去保存,一直往上委托直到DecorView,最后它再一一通知它的子元素来保存数据。典型的委托思想。
如果onRestoreInstanceState被调用 ,那它的onSaveInstanceState参数一定有值。
5.在资源不足情况下导致的activity被杀死也会调用onSaveInstanceState。activity有三个优先级:
* 前台activity。
* 可见但非前台activity(如Dialog)
* 后台activity(onstop的情况)。
### 二,启动模式
1.启动模式介绍:
android有四种模式,分别是standard,singleTop,singleTask,singleInstance。
standard:一个任务栈可以有多个实例,每个实例可以属于不同任务栈。每新建一个activity就新建一个实例。一个实例被哪个实例创建,就存在哪个实例所在的栈中,如A启动了B,那么B就位于A的栈中。不能在appliactionContext中启动它,因为非activity的context没有任务栈,解决方法是加上FLAG_ACTIVITY_NEW_TASK标识,会新建一个任务栈。
singleTop:如果activity存在栈顶,此时activity不会被重建,它的oncreate等方法不会被重调用 。如栈中有ABC,再启动C,就不会重新创建C。
singleTask:栈内复用模式,首先判断所需要的栈是否在否存在,若存在,那么只要在栈中存在,就不新建实例,并cleanTop,如果不存在,新建实例。如果所要的栈都不存在,新建栈。如栈中有ABCD,启动C,C指定了所需的栈且栈不存在,那么就会创建一个新栈,并把C存入,此时就存在两个栈,一个是ABC,一个是C。如栈中有ABCD,启动的C所需的栈就是ABCD所在的栈,那就会把C置于栈项,C上的所有实例出栈,此时栈就变成了ABC。那么如何指定所需任务栈。一般使用TaskAffinity来指定,下面会说明。
singleInstance:具有singleTask的所有特性。不同的是,具有此模式的acitivity只能单独位于一个栈中。如创建了A,A在一个栈中,后续再创建A就不会再创建实例了。
指定启动模式有两种设置方法:
在xml中设置:andorid:launchMode="singleTop"
在代码中设置:intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);这种方法的优点是优先级比第一种高,比如同时用两种方法设置,那以第二种为主,缺点是它不能设置singleInstance。
2.所需任务栈:
TaskAffinity:标识了一个activity所需要的任务栈名称。默认为应用的包名。主要和singleTask配对使用。
任务栈分为前台和后台任务栈。后台 任务栈是指位于暂停的状态。用户可以切换将后台再次调到前台。
A启动B,则B会位于A的任务栈中。(没有TaskAffinity情况下)
比如A启动B,B的TaskAffinity与A的不同,那就会创建新任务栈并把B放入。
AB是前台栈,CD是后台栈,B启动D,则变成ABCD,启动C则变成ABC.
3.标志位:
我们经常会在代码中指定标志位,主要的标志位有以下几种:
FLAG_ACTIVITY_NEW_TASK:类似singleTask
FLAG_ACTIVITY_SINGLE_TOP:类似singleTop
FLAG_ACTIVITY_CLEAN_TOP:清当前activity栈中它本身以上的activity
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:具有这个标记的activity不会出现在历史activity列表中。只需设置
android:excludeFromRecents="true"
### 三,IntentFilter匹配规则
启动activity有显式和隐式两种,如果是隐式,就要满足IntentFilter匹配规则。
intent 有action,category,data.三个属性。只有一个intent同时匹配了action,category和data才算完全匹配。有多个filter只要有一个匹配就可以。
action只要有一个匹配就可以。
category:如果intent有这个属性,那它所有的category都必须匹配上。可以没有。没有默认有DEFAULT这个属性。为了我们的activity能接受隐式调用,就必须加上android.intent.category.DEFAULT这个属性。
data:与action类似。它为分为两部分。mimeType,URL.mineType指媒体类型,如image/jpeg等,URL如下
[http://www.baidu.com:80/search/info](http://www.baidu.com/search/info)
URL默认是content和file,在不指定data 情况下就是匹配它。调用的方法如下
intent.setDataAndType(Uri.parse("file://abc"),"image/png")
完整的代码如下:
~~~
Intent intent = new Intent("com.lxj.a");
intent.addCategory("com.lxj.b");
intent.setDataAndType(Uri.parse("file://abc"),"image/png")
startActivity(intent);
匹配的是如下的activity
<activity …………>
<intent-filter>
<action android:name="com.lxj.a"/>
<actegory android:name="com.lxj.b"/>
<actegory android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="image/*"/>
</intent-filter>
<activity/>
~~~
就先写这么多吧,下一篇我会介绍下我对activity启动源码的理解。
Cocos2d_android你所需要知道的一切(下)
最后更新于:2022-04-01 14:26:09
上一篇我们主要讲了Cocos2d_android一些布置游戏场景的知识,这一篇重点讲下Cocos2d_android的事件触发,没看过上一篇的可以先去看下。
[Cocos2d_android你所需要知道的一切](http://blog.csdn.net/u014486880/article/details/50418485)
对于本章我做了个demo,完成各种动画。先看下效果图。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b63447d.jpg)
点击不同的圣诞树会有不同的动画。因为屏幕有限演示的动画有限,但实现思路都是一样的。
### 一,Action的简介
Cocos2d_android的CCAction总共有四类子类,它们分别是CCFiniteTimeAction,CCFollow,CCRepeatForever,CCSpeed。它们的含义如下:
CCFiniteTimeAction:表示受时间限制的动作,这可以说是最常见的,因为通常动画都是需要一定时间来完成的。它有两个主要的子类,CCFiniteTimeAction和CCIntervalAction,前者是瞬时动作,后者是延时动作。
CCFollow:表示不受时间限制的动作。
CCRepeatForever:表示序列帧播放,非常常用。
CCSpeed:用于控制游戏的速度。
### 二,CCIntervalAction常见动作
**1.移动**
移动是游戏中最常见的动作之一,如植物大战僵尸中的僵尸,子弹等。而Cocos2d中设置移动也很简单,看以下代码:
~~~
CGPoint point = CGPoint.ccp(300, 150);
CCSprite sprite = CCSprite.sprite("bg.img");
CCMoveTo moveTo = CCMoveTo. action(1, point);
sprite.runAction(moveTo);
~~~
CGPoint.ccp就是初始化一个点,CCMoveTo.action就是就是设置sprite通过1秒的时间移动到point上。很简单吧。最后runAction执行动作。
这里还关系到一个MoveTo和MoveBy的区别。它们的区别有两点:
* 简单说MoveTo就是移动到绝对位置,如上面的例子就是移动到(300,150)这个坐标点,而MoveBy就是相对位置,如果当前位置是(100,0),那执行上面的代码最终会移动到(400,150)的位置上,也就是相对当前位置的(300,150)所在的点。
* MoveBy中有一个reverse()方法,可以让动作反向执行,而MoveTo没有。
MoveBy的代码与MoveTo类似:
~~~
CGPoint point = CGPoint.ccp(300, 150);
CCSprite sprite = CCSprite.sprite("bg.img");
CCMoveBy moveBy = CCMoveBy. action(1, point);
sprite.runAction(moveBy);
~~~
再看看调用reverse()的情况,具体运动状态可以看我的demo的第四棵树。
~~~
CGPoint point = CGPoint.ccp(300, 150);
CCSprite sprite = CCSprite.sprite("bg.img");
CCMoveBy moveBy = CCMoveBy. action(1, point);
<pre name="code" class="java" style="font-size: 16px; orphans: 2; widows: 2;">CCMoveBy moveBy1 = moveBy.reverse() ;
sprite.runAction(moveBy);
~~~
由于By与To的代码很类似,下文就只贴一种类型的代码了。不然会累死,哈哈。
**2.旋转旋转的实现也很简单,与Move类似,看下代码:**
~~~
CCRotateBy scaleBy = CCRotateBy.action(0.2f, 360);//100是顺时针100度
sprite.runAction(scaleBy);
~~~
第一个参数依然是时间,第二个参数是度数。旋转是顺时针的。可以看demo的第二棵树。
这里By与To又有点不同,To比较懒,如果是大小180度,它会逆时间转,它的原则是尽量少转。By就比较老实,写多少度,它就转多少。
**3.缩放**
就是缩小放大这个动作,看我demo中的每一棵树,这树的动作并不是一次性的,而是像个心脏似的一起跳,这里就有两个问题,如何让放大缩小两个动作串联起来?如何使动作循环执行?先不急,后面会系统讲解,先带着这问题继续往下。缩放的代码:
~~~
CCScaleBy scaleBy = CCScaleBy.action(0.2f, 1.5f);
sprite.runAction(scaleBy);
~~~
**4.跳跃**
玩过超级玛丽的都知道,小玛丽跳的高度和大玛丽跳的不一样,有些牛X的还能二连跳。其实这个实现起来也很方便,先看一连跳:
~~~
CGPoint pos = CGPoint.ccp (300, 150);
// 跳跃:启动;时间;目标点;高度:实际跳跃的高度(最高点);次数
CCJumpBy jumpBy = CCJumpBy. action(2, pos, 100, 1);
CCSprite sprite = getSprite();
sprite.runAction(jumpBy);
~~~
pos指定最终跳到的点,CCJumpBy.action第一个参数是时间,第二个参数是终点,第三个参数是跳到的高度(最高点),最后一个参数是指跳的次数,由此可见,二连跳就很简单了,只要把1修改为2就可以了。
**5.贝叶斯曲线**
这个听起来很高端,其实就是抛物线状。植物大战僵尸里的花吐出的阳光就走了一条抛物线。实现如下:
~~~
CCBezierConfig c = new CCBezierConfig();
c. controlPoint_1 = CGPoint. ccp(0, 0);
c. controlPoint_2 = CGPoint. ccp(150, 200);
c. endPosition = CGPoint. ccp(300, 0);
CCBezierBy bezierBy = CCBezierBy. action(2, c);
getSprite().runAction(bezierBy);
~~~
**6.渐快渐慢动作**
这个经常用在打斗游戏中,一个怪快速跳到另一个怪身边,杀杀,然后再跳回来,就是一个渐快到渐慢的过程。看下它的代码:
~~~
CGPoint pos = CGPoint.ccp (300, 200);
CCMoveBy moveBy = CCMoveBy. action(2, pos);
CCEaseIn easeIn = CCEaseIn. action(moveBy, 10);// 渐快:加速运动(加速度恒定)
CCEaseOut easeOut = (CCEaseOut) easeIn.reverse();
getSprite().runAction(
CCRepeatForever.action(CCSequence.actions(easeIn,
CCDelayTime. action(1), easeOut)));
~~~
可能上面的代码有几处地方不懂,下面会讲解。先总结下基本的运行方法:
移动动作:CCMoveBy CCMoveTo
缩放动作:CCScaleBy CCScaleTo
旋转动作:CCRotateBy CCRotateTo
跳跃动作:CCJumpBy CCJumpTo
贝赛尔曲线动作:CCBezierBy CCBezierTo
淡入淡出动作:CCFadeBy CCFadeIn CCFadeOut
### 三,组合动作
上部分分个讲了一些常见的动作,但真实游戏实现单个动作的比较少,一般都是串在一起实现的。所以组合动作再所难免。先来看几个常用的接口:
CCSequence:用于将多个动作串联到一起。
CCRepeatForever:用于把一个动作循环执行。
CCDelayTime:设置延时时间。
CCSpawn:并行执行动画。
看到这,上面心跳的那个例子就可以实现了,如下:
~~~
CCScaleBy scaleBy = CCScaleBy.action(0.2f, 1.5f);
CCSequence ccSequence = CCSequence. actions(scaleBy, scaleBy.reverse());
CCRepeatForever ccRepeatForever = CCRepeatForever.action(ccSequence);
sprite.runAction(ccRepeatForever);
~~~
接下来我们用以上几个接口来共同实现一个复杂动作,一棵树翻两个跟斗,具体可以看demo的第三棵树。先看代码:
~~~
// 跳跃:启动;时间;目标点;高度:实际跳跃的高度(最高点);次数
CCJumpBy jumpBy = CCJumpBy. action(2, pos, 100, 2);
CCRotateBy scaleBy = CCRotateBy. action(0.2f, 100);//100是顺时针100度
//并行执行动画
CCSpawn ccSpawn = CCSpawn. actions(jumpBy, scaleBy);
//每循环一次停一秒
CCSequence ccSequence = CCSequence. actions(ccSpawn, ccSpawn.reverse(),CCDelayTime.action(1));
CCRepeatForever ccRepeatForever = CCRepeatForever.action(ccSequence);
sprite.setAnchorPoint(0.5F, 0.5F);
sprite.runAction(ccRepeatForever);
~~~
CCSpawn让跳和旋转两个动作同时执行,然后调用CCSequence再串联一个回来的动作,调用CCDelayTime停一秒后,再经CCRepeatForever继续循环执行动作。
到此已经可以完成很多复杂的动画了。在这里问一个问题,如果我想在某个动作结束后执行某个方法,该怎办?这里要强调的是,动画的执行是在多线程的,所以你直接把方法写在动作后面是行不通的。所以要通过CCCallFunc.action(this, "loadInfo")来调用自定义的方法,这里的方法名是loadInfo。代码如下:
~~~
CCSequence sequence = CCSequence.actions(CCDelayTime.action(1),
CCCallFunc.action(this, "loadInfo"));
sprite.runAction(sequence);
~~~
这是1秒后执行loadInfo方法。
### 四 序列帧播放
什么时序列帧?就是一系列的图片连到一起组成一个动话,就叫序列帧。如一只怪的头点一下,就要有好几张图片串起来实现它的头慢慢下降。先看例子再进行讲解:
~~~
CCSprite bar = CCSprite.sprite("image/loading/loading_01.png");
bar.setAnchorPoint( cgSize. width/2, 25);
this.addChild(bar);
//以下是序列帧
List<CCSpriteFrame> frames = new ArrayList<CCSpriteFrame>();
String fileName = "image/loading/loading_%02d.png" ;//用占位符表示图片尾部,格式为两位数,如01,11。
for( int i = 1 ;i <= 9;i++){
CCSprite frame = CCSprite.sprite(String.format(fileName, i));
frames.add(frame.displayedFrame());
}
CCAnimation animation = CCAnimation. animation("", 0.2f,(ArrayList<CCSpriteFrame>)frames);
CCAnimate animate = CCAnimate.action(animation,false);//false表示只加载一次
bar.runAction(animate);
~~~
首先加载初始状态。把一系列的精灵转成CCSpriteFrame格式的转入到CCAnimation中,它就会自动播放。fileName中的%2d是规定了两位,d是点位符,传什么数进来就显示什么。比如,传1,就显示01,传11就显示11。最后CCAnmate.action中的false表示只加载一次,即动画只执行一次。如果要循环执行,就把它改为true。
到这里,基础知识就介绍的差不多了,可以尝试去玩下植物大战僵尸,代码在上一篇博客中提供了。
源码下载
CoCos2d_android入门所需知道的一切
最后更新于:2022-04-01 14:26:07
做游戏是个很好玩的事情,由于兴趣我最近也在学习如何用android做游戏。顺便它选择了植物大战僵尸来当我的嵌入式作业 。熬了两天两夜终于把它完成了。这个过程从学习调研到写代码的各种心塞到完工,花了很多时间,也花了很多精力,但觉得是值得的。本文并不是讲解植物大战僵尸是怎么做的,而是讲解CoCos2d_android常用的基础知识,当然只要知道这些知识也就能够能完成这个游戏,最后会把代码贴出。先看下效果图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b573e53.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b5a5865.jpg)
、
### 一.Cocos2d_android介绍
Cocos2d是一个大家庭,包括Cocos2d-iphone,Cocos2d-x,Cocos2d-javascript等。而在国内,Cocos2d-x则相对领先。在中国的2D手机游戏开发中,Cocos2d-x引擎的份额超过70%。不同家庭成员之间只是语言不同,而实现的接口名称都相同。所以只要学习一个,其它的就都比较好理解了。本文讲的是Coscos2d_android,因为它是用java实现的,所以对我来说学起来比较快。
### 二.Cocos2d_android架构
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b5cc5a1.jpg)
如上图,Cocos2d这游戏引擎主要由图形引擎(Graphic),声音引擎(Audio),物理引擎(Box2d),脚本库以及相关语言等组成。我们重点关注开发过程中需要注意的。先看一张Cocos2d_android的代码结构图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b5ec7d2.jpg)
对于Cocos2d_android只要关注四个部分, CCDirector(导演),CCScene(场景),CCLayout(幕布)以及CCSprite(精灵)。我们可以把它当成在拍电影,顾名思义可以看出它们的作用:
CCDirector:电影中的导演,肯定是负责整部电影拍摄的,它有三个功能,管理CCScene,开线程执行SurfaceView中的绘制行为,设置游戏属性。
CCScene:电影中的场景,当然包括人和背景。可以理解它是根View,layer都必须建立在它之上,有点类似activity与fragment的关系。
CCLayer:场景中的部分图层,离用户最近的一层。游戏过程中始终只有一个layer能获得焦点。每个动作都必须建立在layer上。
CCSprite:精灵,这个可以理解为activity中的一个控件。就是最小的一部分了。平时控制最多的也就是它,所以要重点关注。
### 三.四大组成部分用法
其它先不说,首先你要用Cocos2d_android,你就应该先把包导进来(可以在我的工程下的libs中找到)。接下来讲解下上面讲的四部分如何在代码里面用。首先是CCDirector,直接看MainActivity中CCDirector的设置代码:
~~~
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CCGLSurfaceView surfaceView = new CCGLSurfaceView(this);
setContentView(surfaceView);
director = CCDirector.sharedDirector();
director.attachInView(surfaceView);//开线程
director.setScreenSize(480, 320);
director.setDeviceOrientation(CCDirector.kCCDeviceOrientationLandscapeLeft);//横屏
director.setDisplayFPS(true);//不显示帧率
CCScene scene = CCScene.node();
scene.addChild(new WelComeLayer());
//导演管理场景
director.runWithScene(scene);
}
@Override
protected void onResume() {
director.onResume();
super.onResume();
}
@Override
protected void onPause() {
director.onPause();
super.onPause();
}
@Override
protected void onDestroy() {
director.end();
super.onDestroy();
}
~~~
CCGLSurfaceView继承着SurfaceView,由此可见游戏引擎是用SurfaceView做的。CCDirector是一个单例,调用sharedDirector来获取它,保证全局只有一个CCDirector。初始化完后就要开始工作了,首先attachInView与surfaceView连接,感兴趣的可以进去看下attachInView源码,它跟我们平时使用surfaceView很相似,也是开了个线程,然后在幕布上画图像。这里主要是讲使用,就不翻源码了。setDeviceOrientation设置屏幕横屏。setDisplayFPS设置为true时就会在游戏左下角显示帧率,这个只是在开发时使用。最后很关键的就是初始化一个CCScene,并调用runWithScene把场景加进去。这样CCDirector的初始工作就结束了。
接下来看CCScene,它是根View,初始时经过runWithScene加入到CCDirector中,不同游戏界面可以理解为不同layer,切换layer可以理解为切换activity一样,而切换的代码如下,基本可以看成是固定格式的。
~~~
CCScene scene = CCScene.node();
scene.addChild(new FightLayer());
CCFadeTransition transition = CCFadeTransition.transition(0.5F, scene);
//替换场景
CCDirector.sharedDirector().replaceScene(transition);
~~~
transition是一个切换动画,CCFadeTransitiion只是切换动画的一种,具体可以察看api。
好,我们要玩游戏就要有界面,界面画在哪上面?xml,还是activity。那么接下来这位成员就派上它的用场了。layer需要我们自己来实现,它要继承自CCLayer,如以下是自定义的layer:
~~~
public class WelComeLayer extends CCLayer {
public WelComeLayer(){
init();
}}
~~~
只要把你自己的实现写在init()方法中就可以了。什么时候被调用就要看什么时候它被addChild到scene中。layer中就可以处理很多事情,包括地图的加载,音乐的播放,精灵的移动等。这些后面会细讲。最后就是精灵啦。
CCSprite是用的最多的,一个僵尸可以是一个精灵,一张图片一段文字都可以是一个精灵。精灵的加载如下:
~~~
choseContainer = CCSprite.sprite("fight_chose.png");
choseContainer.setAnchorPoint(0, 1);
choseContainer.setPosition(0, cgSize.height);
this.addChild(choseContainer,0,1);
~~~
这里是把assets下的一张图片当作精灵,setAnchorPoint是设置锚点,(0,1)就是图片的左上角,相当于图片钉在左上角上,移动旋转时它都为中心。setPosition不用说,就是设置精灵的位置啦。然后调用layer的addChild就把它加载进来了。这里它有三个参数,第一个是精灵,第二个是它的层数,0是最底层的,如果想把它置于上层,就给它设置一个值,值越大层数越高,就越不会被覆盖。最后一个参数是tag,设置了它就可以在其它地方通过tag来获取这个精灵,类似于findViewById(),获取代码如下:
~~~
CCSprite c = layer.getChildByTag(1);
~~~
注意上面的layer必须是你定义精灵所在的layer中。那么如何在layer中监听点击事件呢,看以下代码:
~~~
//设置物体的触发事件
@Override
public boolean ccTouchesBegan(MotionEvent event) {
// TODO Auto-generated method stub
CCSprite sprite = (CCSprite) this.getChildByTag( TAG_X);
//转成opengl下的坐标点
CGPoint cgPoint = this.convertTouchToNodeSpace(event);
boolean flag = CGRect.containsPoint(sprite.getBoundingBox(), cgPoint);
if(flag){
//设置透明度
sprite.setOpacity(100);
//设置是否可见
sprite.setVisible( false);
//删除自己
sprite.removeSelf();
}
return super.ccTouchesBegan(event);
}
~~~
类似于OnEventTouch(),这里有一个知识点,就是android平时的习惯是把屏幕左上角当坐标原点,向下的y正,向右是x正。但是在Cocos2d中不一样,你必须把它转成openGl的习惯,也就是左下角是坐标,向上是y正,向右是x正。转化也有api,如上面的convertTouchToNodeSpace()就是把android的point转成OpenGL下的Point。
### 四,加载资源文件
讲到这里,我假设你之前的都己经明白了,也能在不同界面上显示不同的sprite了,那接下来我们就要对游戏场景进行优化,首先光一些精灵肯定不行,肯定要有背景图,而且游戏嘛,肯定也要有音乐。那首先来讲讲地图的加载吧。
其实地图加载我认为一点都不简单,别以为地图只是一张图片,如果是张图片,那游戏开发过程中要定位位置怎办。其实在游戏中有一个格式的文件很见,.tmx文件,它包括地图上某些你标记的点的信息。这样的图片用文本编辑器打开如下:
~~~
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.0" orientation="orthogonal" width="14" height="6" tilewidth="46" tileheight="54">
<tileset firstgid="1" name="bk1" tilewidth="46" tileheight="54">
<image source="bk1.jpg" width="678" height="331"/>
</tileset>
<layer name="block" width="14" height="6">
<data encoding="base64" compression="zlib">
eJwNw4lSQQEAAMAXEZWEpKjooEShkhJdEkmS+v9vaXdml4IgCBl22YhRV4wZd9U11024YdJNU6bNuGXWbXPuuGvegnvue2DRkoceeeyJZSueembVc2vWvfDShk1bXnntjW1v7XjnvV0f7Nn30SefffHVgW8OfXfk2A8nfjr1y5nfzv1x4a9//gNAug3z
</data>
</layer>
<objectgroup name="road" width="14" height="6" visible="0">
<object x="473" y="96"/>
<object x="474" y="306"/>
<object x="23" y="303"/>
</objectgroup>
</map>
~~~
它会被解析成一个xml格式的文件,里面记录了地图长宽,及长宽分为几块,以及一些标记点的集合。如上面name为“road”的集合。看到这你是不是吓到了,难道每个地图都要自己手写?这肯定是不可能的。这里介绍一款小软件,tiled([下载链接](http://u.download.csdn.net/upload/success))。它可以方便的进行地图标记,显示如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b60d26c.jpg)
如果你想改变背景图的样子,可以用上边栏的填充工具等进行绘制,如果想定点就新建一个对象层,如同图中的road。再用工具栏上的创建对象按钮在地图上定点,记住定点的顺序是有讲究的,先确定的点会存在前面,这个待会解析时会说,可以创建多个对象层,起的名字就是文件中objectgroup的name属性对应的名字。图片制作完成后保存就自动更改为.tmx格式。把它放到assets文件下,就可以加载它了,来看下加载的代码:
~~~
map = CCTMXTiledMap.tiledMap("map_day.tmx");
map.setAnchorPoint(0.5f,0.5f);
CGSize contentSize = map.getContentSize();
//左移右移一半
map.setPosition(contentSize.width/2,contentSize.height/2);
this.addChild(map);
~~~
CCTMXTiledMap.tiledMap就是把图片加载进来,getContentSize就是获取地图的大小,之所以把地图的位置置为大小的一半,是因为地图可能大于屏幕,这样设置希望它一开始就能显示左下部分。那如何获得我们在地图上设置的点呢,如下:
~~~
CCTMXObjectGroup zombiesGroup = map.objectGroupNamed("road");
ArrayList<HashMap<String, String>> zombies = zombiesGroup.objects;
// 分别以x和y为键,获取坐标值信息---->封装到点集合中
List<CGPoint> points = new ArrayList<CGPoint>();
for (HashMap<String, String> item : zombies) {
float x = Float.parseFloat(item.get("x"));
float y = Float.parseFloat(item.get("y"));
points.add(CGPoint.ccp(x, y));
}
~~~
这个可以说是固定格式,你可以把它写到工具类中,最后points这个list中点信息的顺序就是你在地图上添加点的顺序,所以添点忌随意。有了点你就能干很多事,如设置移动路线。地图加载讲完后,就是音乐加载了。
这样相对比较简单,音乐的话要获得一个音乐引擎来加载音乐。音乐引擎其实就是封装了下mediaPlayer而已。看下代码:
~~~
SoundEngine engine = SoundEngine.sharedEngine();
engine.preloadSound(getContext(), R.raw.start);
~~~
~~~
engine.playSound(getContext(), com.lxj.zhiwuvsani.R.raw.start, true);
~~~
如上,raw文件下放一些音乐文件,preloadSound是预先加载,可以把它放在游戏加载过程中,而playSound就是你想在哪播就设置在哪。true表示循环播放。
最后,说下字体的加载吧,Cocos2d_android字体用的是CCLabel,它继承自CCSprite,设置如下:
~~~
CCLabel cLabel = CCLabel.makeLabel("hello world", "Roboto_Thin.ttf", 20);//创建字体,中间参数为ttf文件,20为字体大小
cLabel.setPosition(cgSize.width/2,cgSize.height/2);
this.addChild(cLabel,1);
~~~
它加载的字是hello world,加载的字体格式是Roboto_Thin.ttf,这个文件要存在assets文件夹下,如有不想设置那就直接置为“”。
讲了这么多,现在己经可以自己布置场景,加载精灵任意摆放,并配上音乐字体了。一个游戏界面就差不多了,但是,很重要的一点,东西都不会动!!!!,不会动怎么玩。下一篇我将讲讲如何让精灵动起来,设置各种action及判断位置。看完下篇,你就能完整的做游戏了。当然看这些可能还不够,这些只是最常用的,对于一些不常用的不懂的时还要去查下api,好啦,说了这么多,好累,歇会。
[植物大战僵尸源码下载](https://github.com/reallin/CoCos2d_android_PVZ)
IPC——android进程间通信
最后更新于:2022-04-01 14:26:05
### 一,什么是IPC
IPC:inter-process communication,进程间通信或者跨进程通信。window通过剪贴板,管道等进行进程间通信。Linux通过命名管道,共享内存,信号量等进行进程间通信。android有特色的是Binder。在android进程通信可以有以下方式:aidl,socket通信,使用Bundle,使用contentprovider,使用Messenger,使用文件共享。
### 二,android的序列化
在android中能用于进程间通信的对象必须是序列化的。序列的方式有两种,一种是实现Serializable接口,一种是实现Parcelable。Serializable是java提供的序列化方法,实现很简单,只要一个类实现了它就完成了序列化。可用于将对象序列化到存储设备或序列化后通过网络传输。但它的效率不高,这里重点介绍Parcelable,它是android自己的实现序列化方法,先看实现Parcelable的代码:
只要一个类实现了这个接口,就可以实现序列化并可以通过Intent和Binder传递。要实现以下方法
(1).writeToParcel(out,flags):实现序列化功能,一系列write。如下
out.writeInt(userId);
out.writeParcelable(book,0);//传另一个序列化类
(2).CREATOR:反序列化,一系列的read方法:如下
userId = in.readInt();
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
(3).decribeContents:内容描述,几乎所有情况都返回0.
完整代码如下
~~~
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book() {
}
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
public int describeContents() {
return 0;
}
public void writeToParcel(Parcel out, int flags) {
out.writeInt( bookId);
out.writeString( bookName);
}
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
public Book createFromParcel (Parcel in) {
return new Book(in);
}
public Book[] newArray( int size) {
return new Book[size];
}
};
private Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}
@Override
public String toString() {
return String.format("[bookId:%s, bookName:%s]" , bookId , bookName);
}
}
~~~
系统还提供了一些实现了Parcelable接口的类,如Intent,Bundle,Bitmap.同时List和Map也可以序列化,前提是它们里所有元素都 可以序列化。
Parcelable效率比Serializable高,在内存序列化上用它合适。但在将对象序列化到存储设备或序列化后通过网络传输则建议用Serializable,比较方便。
### 三,Binder的介绍
Binder用于完成进程间的通信(IPC)。Binder是工作在Linux层,属于一个驱动,这个驱动不需要驱动,Binder代码运行在内核态,调用Binder是系统进行调用。
Binder是一种架构,这种架构提供服务端接口,Binder 接口,客户端接口三个模块。一个Binder 服务端就是一个Binder对象,该对象一旦创建,就会启动一个隐藏线程,该线程会接Binder 驱动发送的消息。收到消息会发调用 onTransact()方法,并按照参数执行不同的服务代码,onTransact() 的参数来源是客户端调用 transact(),若transact() 有固定的输入,onTransact()就有固定的输出 。
一个服务端被创建,会创建一个mRemote对象,它的类型也是Binder类,客户端要访问远程时是通过它。它也重载了transact()方法,重载内容如下:
1,以线程消息通信模式,向服务端发送客户端传递过来的参数。
2.挂起当前线程,并等待服务端线程执行完指定服务函数后通知。
3.接收服务端通知并执行客户端线程,并返回到客户端代码区。
它的调用图如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b55b898.jpg)
接下来我们来看看如何使用它。
新建两个文件Book.aidl,IBookManager.aidl,其中Book.aidl是上面序列化后文件的一个声明,主意名称要和序列化的类的名称一样。内容如下:
~~~
package com.lxj.aidl;
parcelable Book;
~~~
再看下IBookManager.aidl,这个demo主要实现两个方法,所以都需要这此声明:
~~~
package com.lxj.aidl;
import com.lxj.aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
~~~
除了基本数据类型外,其它数据类型都要以in out inout来标识,in表示输入,out表示输出,inout即可表示输入也可表示输出。
这时系统会自动生成一个IBookManager.java文件,内容如下:
~~~
package com.lxj.aidl;
public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.lxj.aidl.IBookManager
{
private static final java.lang.String DESCRIPTOR = "com.lxj.aidl.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.lxj.aidl.IBookManager interface,
* generating a proxy if needed.
*/
public static com.lxj.aidl.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.lxj.aidl.IBookManager))) {
return ((com.lxj.aidl.IBookManager)iin);
}
return new com.lxj.aidl.IBookManager.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.lxj.aidl.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.lxj.aidl.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.lxj.aidl.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.lxj.aidl.IBookManager
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public java.util.List<com.lxj.aidl.Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.lxj.aidl.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.lxj.aidl.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public void addBook(com.lxj.aidl.Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public java.util.List<com.lxj.aidl.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.lxj.aidl.Book book) throws android.os.RemoteException;
}
~~~
asInterface:用于将服务端的Binder对象转换成客户端所需的AIDL对象,同一进程返回的是服务端的Stub,不同进程返回系统封闭后的Stub.proxy对象。
asBinder:用于返回当前 的Binder对象。
onTransact(code,data,reply,flags):运行在服务端的Binder线程池中,客户端发出的请求会在此处理。code可以确定客户端的请求目标方法是什么,data取出参数,运行完把结果赋给reply。如果返回false,则客户端请求会失败(可用于权限判断)。
getBookList:首先创建输入输出的Parcel对象_data,_reply。接着调用transact方法来发起RPC(远程过程调用)请求,同时挂起线程,然后服务端onTransact会调用,直到RPC执行完,当前线程继续执行,并从_reply取出RPC结果。返回。
### 四,AIDL进程间调用
aidl是最常见的进程调用方式。我们先创建两个应用,模拟进程间通信(不同应用)。客户端的代码如下:
~~~
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent("com.lxj.servicetest.MyAIDLService");
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
// TODO Auto-generated method stub
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// TODO Auto-generated method stub
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = bookManager.getBookList();
Log.e("list", list.toString());
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
~~~
通过bindService绑定服务端Binder,然后通过IBookManager.Stub.asInterface()来获得服务端返回的AIDL对象,就可以调用到服务端的方法(getBookList)。再看下服务端代码:
~~~
public class MyBookService extends Service {
private CopyOnWriteArrayList<Book> mBooks = new CopyOnWriteArrayList<Book>();
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
mBooks.add(new Book(1,"java虚拟机"));
mBooks.add(new Book(2,"android疯狂讲义"));
}
private Binder mBinder = new IBookManager.Stub() {
//虽然IBinder只能传输List,但是服务端之所以可以用CopyOnWriteArrayList是因为它会自动在传输时转化为list
@Override
public List<Book> getBookList() throws RemoteException {
// TODO Auto-generated method stub
return mBooks;
}
@Override
public void addBook(Book book) throws RemoteException {
// TODO Auto-generated method stub
mBooks.add(book);
}
};
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return mBinder;
}
}
~~~
自定义了一个Service并在list中添加两本书,所以客户端就能在控制台上打印出这两本书。注意要在AndroidFest.xml添加上Service的声明:
~~~
<service
android:name="com.lxj.aidl.MyBookService"
>
<intent-filter>
<action android:name="com.lxj.servicetest.MyAIDLService"/>
</intent-filter>
</service>
~~~
### 五,Messenger进程间通信
android提供Messenger可以实现不同进程间的信息传递,相比AIDL它的调用简单很多,但是它是有缺点的,它只能串行处理发送的信息,如里信息过多就会阻塞。其次它只能传输基本数据类型,What,arg1,Bundle等,并不能传输自定义的对象。Messenger是一种种轻量级的IPC方案,一次处理一个请求。
1.服务端:
创建一个Service来处理客户端连接请求,同时创建一个Handler并通过它来创建一个Message对象,然后在OnBinder中返回这个Binder。
2.客户端:
首先绑定服务端Service,通过返回的IBinder创建一个messager。这个就可以给服务端发消息了,如果需要服务端返回,则也要跟服务端一个创建Handler,通过Message的replyTo参数传递给服务端。
先看下客户端的代码:
先看客户端代码:
~~~
public class MainActivity extends Activity {
TextView txvTextView ;
private Messenger mService;
private Messenger clientMessenger = new Messenger(new MessengerHandler());
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch(msg.what){
case 1:
Log.e("msg from service", msg.getData().getString("reply"));
break;
default:
super.handleMessage(msg);
}
}
}
private ServiceConnection connection = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// TODO Auto-generated method stub
mService = new Messenger(service);
Message msgMessage = Message.obtain(null,0);
Bundle b = new Bundle();
b.putString("client", "Hi,i am client");
msgMessage.setData(b);
msgMessage.replyTo = clientMessenger;
try {
mService.send(msgMessage);
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
// TODO Auto-generated method stub
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txvTextView = (TextView)super.findViewById(R.id.txv);
Intent i = new Intent(this,MessengerService.class);
bindService(i, connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(connection);
super.onDestroy();
}
}
~~~
看到上面的代码我们可以很清楚的看到,Messenger也是用AIDL实现进程间通信的。通过Messenger发送消息到服务端,handler中获取服务端返回的replyto。再看下服务端代码:
~~~
public class MessengerService extends Service{
private final Messenger mMessenger = new Messenger(new MessengerHandler());
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch(msg.what){
case 0:
Log.e("msg from client", msg.getData().getString("client"));
Message message = Message.obtain(null,1);
Messenger messenger = msg.replyTo;//从客户端得到的messenger,用于在客户端中显示
Bundle bundle = new Bundle();
bundle.putString("reply", "Hi,i am service");
message.setData(bundle);
try {
Thread.sleep(1000);
messenger.send(message);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return mMessenger.getBinder();
}
}
~~~
这里就是获取到信息后再把信息发送回客户端。完成了不同进程的信息传递。
这里只是简单介绍两种IPC方式,还有很多常用方式如socket通信,ContentProvider等都可以实现IPC。本文只是总结下自己对IPC的认识。
Android的消息处理机制,AsyncTask源码解析
最后更新于:2022-04-01 14:26:02
之前写过一篇Handler的源码解析文章,因为AsyncTask底层也是Handler实现的,所以不了解的可以先去了解下Handler。本文也会再次分析下Handler,毕竟它是android源码中随处可见的东东。
### 一、Handler的简要分析
讲Handler之前我们先讲一下ThreadLocal的概念。简单的说,ThreadLocal是介于局部变量和全局变量之间,可以在不同线程中互不干扰地存储并提供数据。也就是说,变量a在A线程中值与在B线程中值可能是不同的。它通过get()和set()方法来获取和存储变量。
由于之前分析过Handler,在这里就大概的介绍,Handler底层是由MessageQueue和Looper去支撑。Looper可以理解为一个中介,负责论询MessageQueue中的消息,并它消息传给Handler进行处理。MessageQueue内部存储结构是一个单链表,可以快速的插入和删除。
在ActivityThread启动过程中就会创建Looper并存放在ThreadLocal中,在Handler的构造方法中就会通过Looper.myLooper()去ThreadLocal中获取该线程的Looper,接下来调用looper.prepare()方法,在该方法中创建MessageQueue,在 handler调用post或者sendMessage()时,就会调用enqueueMessage把消息存到单链表中,而looper.loop()方法其实就是一个死循环,一直在论询MessageQueue,如果获得到消息就调用target(其实就是handler)的dispatchMessage()并调用 handleMessage()进行消息处理。以上就是Handler的源码流程,如果感兴趣可以自行去跟一下。这里有一点需要强调下,就是在处理dispatchMessage()时,它的源码如下:
~~~
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
~~~
只有当callback为空时才会调用handleMessage,否则调用callback.handleMessage()。这里也给我们提供了一个思路,可以通过callback来拦截消息,比如以下代码:
~~~
private Handler H = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if(msg.what == 0x11){
return true;
}else{
return false;
}
}}) {
@Override
public void handleMessage(Message msg) {
//处理相应业务
};
~~~
这相当msg的what是0x11时,就不会再执行handleMessage了。
使用Handler有一点非常重要的,如果是在子线程使用,那需要手动的添加looper.prepare()和looper.loop()方法。而子线程handler的handleMessage也是在子线程中处理,这点需要处理。
### 二,HandlerThread
上面即然说到在子线程中使用Handler,那我们就来说下HandlerThread,HandlerThread其实就是一个封装好Thread,它可以使用Handler,之所以不提倡在子线程中直接使用Handler是因为它存在并发的问题。也就是说Handler在创建时需要获取Looper对象,但此时可能Thread还没有跑到Looper.prepare()那一步,那就会报空指针异常,看到这里我们第一反应肯定是那就用wait()和notifyAll()来实现啊。是的,HandlerThread就是加入了这两个元素,不信可以翻下源码:
~~~
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
public Looper getLooper() {
if (!isAlive()) {
return null;
}
// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}
~~~
HandlerThread在调用getLooper时,如果还不存在,就wait(),它本身是一个Thread,它在run方法创建完就会调用notifyAll(),这样getLooper就会继续执行。就这么简单。实现代码如下:
~~~
HandlerThread thread = new HandlerThread("handler");
thread.start();
Handler h = new Handler(thread.getLooper()){
@Override
public void handleMessage(Message msg) {
//处理相应业务
};
};
h.sendEmptyMessage(0X11);
~~~
### 三,AsyncTask源码分析
上面说了那么多,都是在消息处理机制,感觉有点跑题了,囧。。。接下来就来分析下AsyncTask,终于进入正题了。
**1.用法介绍**
它可以在线程池中执行后台操作,并把进度结果传递给主线程。AsyncTask并不适合进行特别耗时的操作,对于耗时的任务来说建议使用线程池。
AsyncTask提供4个核心方法:
**onPreExecute()**:执行前执行。
**doInBackground(Params……params)**:执行任务,可通过publishProgress方法更新任务进度,publishProgress会调用onProgressUpdate,返回计算结果给onPostExecute。
**onProgressUpdate(Progress……values)**:在主线程中执行,当后台任务执行进度发生改变时会调用。
onPostExecute(Result result):在任务执行之间调用。
它最基本的用法如下所示:
~~~
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
protected Long doInBackground(URL... urls) {
int count = urls. length;
long totalSize = 0;
for ( int i = 0; i < count; i++) {
// totalSize += Downloader.downloadFile(urls[i]);
publishProgress(( int) ((i / ( float) count) * 100));
// Escape early if cancel() is called
if (isCancelled())
break;
}
return totalSize;
}
protected void onProgressUpdate(Integer... progress) {
// setProgressPercent(progress[0]);
}
protected void onPostExecute(Long result) {
// showDialog("Downloaded " + result + " bytes");
}
}
~~~
好啦 ,讲完用法,来分析下它的源码吧!!
**2.源码分析**
从它的execute方法入手。
~~~
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
~~~
sDefaultExecutor就是一个线程池,一个进程的所有线程都会在这个串行的线程池中排队。接着我们进入executeOnExecutor中看看:
~~~
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}
mStatus = Status.RUNNING;
onPreExecute();
mWorker.mParams = params;
exec.execute(mFuture);
return this;
}
~~~
可以发现onPreExecute()最先执行。这也应验了我们之前的分析。接下来就调用了线程池执行代码,来看下AsyncTask线程池的实现。
~~~
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
~~~
系统会把Params封装成FutureTask类,这是一个并发类,相当于Runnable,接着会在SerialExcutor的execute中执行,如果没有正在活动的AsyncTask任务,就调用scheduleNext()执行下一个,从队列中poll对象执行,直到完成。所以它是串行的(注意,3.0以后AsyncTask是串行执行的)。
AsyncTask有两个线程池,(serialExecutor,THREAD_POOL_EXCUTOR),serialExecutor用于任务排队,THREAD_POOL_EXCUTOR用于执行任务。着重来看下THREAD_POOL_EXCUTOR。
~~~
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
~~~
~~~
<pre name="code" class="java">public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
~~~
有没有很熟悉,上篇博客就分析过线程池的用法,它的核心线程是CPU数量+1,最大线程数是2CPU+1,超时时间是一秒。由于FutureTask的run方法会调用mWorkder的call方法,所以就调用如下代码:
~~~
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
return postResult(doInBackground(mParams));
}
};
~~~
先设置mTaskInvoked为true,表示当前任务己经被调用过了。然后执行doInBackground()方法,并把结果返回给postResult();所以子线程的业务就在doInBackground方法中执行。结果有两种情况,后面会再讲到,先来看下postResult如下:
~~~
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}
~~~
看到这里就明白了,它的底层还是Handler。这里就不再分析Handler了。主要找到handler,看getHandler()的方法。
~~~
private static Handler getHandler() {
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler();
}
return sHandler;
}
}
~~~
很直然就进入InternalHandler()。
~~~
private static class InternalHandler extends Handler {
public InternalHandler() {
super(Looper.getMainLooper());
}
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}
~~~
两种情况,一种是运行结束,调用finish,一种是还没结束,调用onProgressUpdate传入进度值。是不是一下子就把三个方法串起来了。好了,到这里AsyncTask的源码就分析完了。
总结下就是AsyncTask底层由Handler来完成线程的切换,内部由线程池来执行。有两个线程池,一个用于任务排除,一个用于线程执行。
好啦 ,就写到这吧。
浅谈android的线程池
最后更新于:2022-04-01 14:26:00
其实在我眼里,线程池是一个很高端的东西,它会管理很多线程,并在进程中进行多线程的操作,是一个很高效且方便使用的东西。本篇文章就说说我对线程池的认识。
### 一,线程池的基本概念
线程池有很多优点,比如避免了重复创建和销毁线程而降低了程序的运行效率,其次它可以很方便的控制线程的最大并发数,在一定程度上可以减少线程间的阻塞等。在android中线程池是由java的Executor实现的。它的真正实现类是ThreadPoolExecutor,下面是它的构造方法和相关介绍。
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnalbe>workQueue,ThreadFactory threadFactory)
**corePoolSize:**核心线程数量,它会一直存在,没任务就处理闲置状态 。如果allowCoreThreadTimeOut设置为true,则闲置的核心线程会有超时策略,时间由keepAliveTime控制,当超时时,核心线程就会被终止。
**maximumPoolSize:**线程池能容纳的最大线程数,当活动数达到这个数时,后续的新任务将会被阻塞。
**keepAliveTime:**超时时非核心线程会被回收,如果allowCoreThreadTimeOut设置为true,核心线程也会被回收。
**unit:**指定keepAliveTime的单位。TimeUnit.MILLISECONDS(毫秒),TimeUnit.SECONDS(秒)等。
**workQueue:**线程中的任务队列,通过线程池的excute方法提交的Runnable会存储到这参数中。
**threadFactory:**线程工厂,为线程池提供创建新线程的功能。
了解了上面的概念后,我们来看下android线程池的分类。
### 二,线程池的分类
1.FixedThreadPool
这是一种线程数量固定的线程池。处理空闲状态时,并不会被回收,除非线程池关闭。它的源码如下:
~~~
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
~~~
由上面介绍的ThreadPoolExecutor可知,FixedThreadPool的核心线程有nThreads条,最大线程也相同,这证明了它没有非核心线程,且FixedThreadPool没有超时策略,所以空闲时线程不会被回收。此外它的任务队列也是无限大的。它适合用于需要快速响应的外界请求的情况下。接下来看下它的用法。
~~~
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
fixedThreadPool.execute(Runnable);
~~~
用法很简单,只需要使用Executors就可创建FixedThreadPool,并初始化线程数。看到这个,以后开线程就不需要再用Thread了。
2.CacheThreadPool
是一种线程数量不定的线程池,只有非核心线程,并且最大线程数可以说是无穷大。当线程池中的线程都处于活动状态时,创建新线程。这类线程适合执行大量耗时较少的任务。看下它的构造方法:
~~~
<pre name="code" class="java"><pre name="code" class="java"> public static final int MAX_VALUE = 0x7FFFFFFF;
~~~
~~~
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
~~~
同样很好理解,没有核心线程,最大线程娄是MAX_VALUE,基本可以理解成无上限,超时机制是60秒,由上分析可知,它在闲置60秒后会被回收,所以基本不占系统资源。看下它的用法。
~~~
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
cachedThreadPool.execute(runnable);
~~~
3.scheduledThreadPool
核心线程数量是固定的,而非核心线程数是没有限制的,并且非核心线程闲置时会被立即回收,可用于执行定时和具有固定周期的重复任务。看下它的构造方法:
~~~
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
~~~
super就相当于调用了ThreadPoolExecutor,看下它的用法:
~~~
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
// 1s后执行command
scheduledThreadPool.schedule(runnable, 1000, TimeUnit.MILLISECONDS);
// 延迟10ms后,每隔1000ms执行一次command
scheduledThreadPool.scheduleAtFixedRate(runnable, 10, 1000, TimeUnit.MILLISECONDS);
~~~
4.SingleThreadExecutor
内部只有一个线程,确保任务都在一个线程中按顺序执行。这个多用于串行处理情况。不用考虑并发的问题。它的构造方法如下:
~~~
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
~~~
没什么好解释的,来看下它的用法:
~~~
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
singleThreadExecutor.execute(runnable);</span>
~~~
分析完所有线程池后,我们得知,不同线程池有不同的应用环境,并没有说哪个比较好。在实际情况中我们要根据自己的需求来适当的选择。其实线程池在android的源码中有大量的使用,下一篇博客将讲解下AsyncTask的源码解析,里面就运用了线程池。
android自定义控件
最后更新于:2022-04-01 14:25:58
最近忙得好久没写博客了,今天再次回归,总结巩固自己学的东西。在android中都觉得写控件是一件比较难的事,其实并不难,这篇博客来讲讲如何自定义控件,并自定了我在项目中常用的一个控件,先看效果图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b531d3a.jpg)
话不多说,直接进入主题。
看看上图的圆框,我们要先画出此界面。布局如下:
~~~
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rel"
android:layout_width="fill_parent"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="@drawable/global_advsearch_item_shape" >
<TextView
android:id="@+id/text1"
android:layout_width="35dp"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerInParent="true"
android:layout_marginLeft="11dp"
android:textColor="#444444"
/>
<View
android:id="@+id/view1"
android:layout_width="1dp"
android:layout_height="15dp"
android:layout_marginLeft="6dp"
android:layout_centerInParent="true"
android:layout_toRightOf="@id/text1"
android:background="#CCCCCC" />
<TextView
android:id="@+id/edit1"
android:layout_toRightOf="@id/view1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerInParent="true"
android:background="@null"
android:layout_marginLeft="13dp"
android:layout_marginRight="13dp"
android:gravity="left|center"
android:hint="请选择"
android:textColor="#444444"
/>
</RelativeLayout>
~~~
外框的背景也要用drawable下的shape来画,定义好弧度和框的颜色及粗度,如下:
~~~
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#FFFFFF" />
<stroke android:color="#F56A55"
android:width="1dp" />
<corners android:radius="100dp" />
</shape>
~~~
既然是自定义的组件,那么组件的属性也要可以设置。需要定义以下的属性文件,在attrs.xml中:
~~~
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="OutButton">
<attr name="btnbackground" format="reference|color"/>
<attr name="leftText" format="string"/>
<attr name="leftSize" format="dimension"/>
<attr name="leftColor" format="color"/>
<attr name="rightColor" format="color"/>
<attr name="rightText" format="string"/>
<attr name="rightSize" format="dimension"/>
</declare-styleable>
</resources>
~~~
以上的属性是随意添加的。其中leftText是控制左textView的文字,leftColor是控制文字的颜色。以此类推。format是定义属性的类型,如string是指属性要定义成字符串,dimension指的是大小,如12sp之类的。color是颜色。reference可以是引用 ,如设置图片背景的时候引用drawable下的文件。
然后就是定义我的组件,为了方便让它继承自FrameLayout,代码 如下:
~~~
public class customButton extends FrameLayout {
private TextView leftText;
private TextView rightText;
private RelativeLayout rel;
private OutClickListener mlistener = null;
private int flag;
public customButton(final Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.outbutton, this);
leftText = (TextView)super.findViewById(R.id.text1);
rightText = (TextView)super.findViewById(R.id.edit1);
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.OutButton);
leftText.setText(ta.getString(R.styleable.OutButton_leftText));
rightText.setText(ta.getString(R.styleable.OutButton_rightText));
leftText.setTextSize(ta.getDimension(R.styleable.OutButton_leftSize, 10));
rightText.setTextSize(ta.getDimension(R.styleable.OutButton_rightSize, 10));
//rel.setBackground(ta.getDrawable(R.styleable.OutButton_btnbackground));
rightText.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(mlistener !=null)
mlistener.popListener();
/*int a = mlistener.getFlag();
Intent i = new Intent(context,CustomDialogActivity.class);
context.ststartActivityForResult(i,1);*/
}
});
ta.recycle();
}
public void setOutClickListener(OutClickListener listener){
this.mlistener = listener;
}
public interface OutClickListener{
void popListener();
//int getFlag();
}
}
~~~
其实很简单,逐一讲解。
~~~
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.OutButton);
~~~
指的是获取我们刚刚定义的属性文件。然后将其中定义的属性赋给控件,这样属性就与控件绑定到一起。为了使控件有点击事件,我们需要定义 一个接口OutClickListener,并在rightText的点击事件中调用接口的方法。最后通过setOnClickListener()把接口暴露给调用者,这样就可以通过回调在外层写点击事件。也保证也控件的解藕。
这样控件就定义完了,来看看如何使用。为了可以使用属性,要在布局文件的命名空间加入如下一行声明:
~~~
xmlns:lxj="http://schemas.android.com/apk/res-auto"
~~~
接下来就可以自定义属性了,如下:
~~~
<com.example.linxj.customoutbutton.customButton
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
lxj:leftText="时间"
lxj:leftSize="8sp"
lxj:rightSize ="8sp"
lxj:rightText="请点击"></com.example.linxj.customoutbutton.customButton>
~~~
这样就可以显示了。接下来我们用这个控件来完成弹出框的功能。在一个项目中,可能会多次很到不同的弹出框,为此可以定义一个Dialog形式的Activity,并实现多个Dialog的复用。先看看代码:
~~~
public class CustomDialogActivity extends Activity {
private List<String> dataList;
private ListView listView;
private Button cancelButton;
private String[] dataSource;
private int flag;
private static final float RATIO = 5/10f;
private String templeContent = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.setContentView(R.layout.global_popwin_main);
dataList = new ArrayList<String>();
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.width = LayoutParams.FILL_PARENT;
lp.gravity = Gravity.BOTTOM;
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
getWindow().setAttributes(lp);
initViews();
}
private void initViews(){
Bundle bundle = getIntent().getExtras();
flag = bundle.getInt("flag");
initData(flag);
cancelButton = (Button) findViewById(R.id.cancel_but);
listView = (ListView) findViewById(R.id.select);
listView.setAdapter(new SessionBaseAdapter());
listView.setOnItemClickListener(new OnItemClickListenerImpl());
setPopWindowSize(flag);
cancelButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putInt("flag", flag);
bundle.putString("content", "");
intent.putExtras(bundle);
setResult(RESULT_OK, intent);
finish();
}
});
}
private void initData(int flag){
dataList = new ArrayList<String>();
switch (flag) {
case 0:
Bundle bundle = getIntent().getExtras();
dataSource = bundle.getStringArray("project_year");
case 1:
dataSource = getResources().getStringArray(R.array.startYear);
break;
}
for (int i = 0; i < dataSource.length; i++) {
dataList.add(dataSource[i]);
}
}
private void setPopWindowSize(int flag){
switch (flag) {
case 0:
case 1:
float screenHeight = this.getResources().getDisplayMetrics().heightPixels;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) listView.getLayoutParams();
layoutParams.height = (int) (screenHeight * RATIO);
listView.setLayoutParams(layoutParams);
break;
}
}
final class ViewHolder {
public ImageView image;
public TextView itemName;
public RelativeLayout layout;
}
class SessionBaseAdapter extends BaseAdapter {
public SessionBaseAdapter() {}
@Override
public int getCount() {
return dataList.size();
}
@Override
public Object getItem(int postion) {
return postion;
}
@Override
public long getItemId(int postion) {
return postion;
}
@Override
public View getView(final int postion, View convertView, ViewGroup parent) {
final ViewHolder holder = new ViewHolder();
convertView = LayoutInflater.from(CustomDialogActivity.this).inflate(R.layout.global_popwin_listitem, null);
holder.layout = (RelativeLayout) convertView.findViewById(R.id.type1_layout_1);
holder.image = (ImageView) convertView.findViewById(R.id.icon);
holder.itemName = (TextView) convertView.findViewById(R.id.text);
convertView.setTag(holder);
holder.itemName.setText(dataList.get(postion));
holder.layout.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
holder.itemName.setTextColor(Color.parseColor("#F56A55"));
holder.image.setVisibility(View.VISIBLE);
templeContent = dataList.get(postion);
startIntent();
}
});
return convertView;
}
}
private void startIntent() {
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putInt("flag", flag);
bundle.putString("content", templeContent);
intent.putExtras(bundle);
setResult(RESULT_OK, intent);
this.finish();
}
public class OnItemClickListenerImpl implements OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int postion, long id) {
ViewHolder holder = (ViewHolder) view.getTag();
holder.image.setVisibility(View.VISIBLE);
holder.itemName.setTextColor(Color.parseColor("#F56A55"));
templeContent = dataList.get(postion);
startIntent();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(CustomDialogActivity.this, event)) {
startIntent();
return true;
}
return super.onTouchEvent(event);
}
private boolean isOutOfBounds(Activity context, MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();
final View decorView = context.getWindow().getDecorView();
return (x < -slop) || (y < -slop)|| (x > (decorView.getWidth() + slop))|| (y > (decorView.getHeight() + slop));
}
@Override
public void onBackPressed() {
super.onBackPressed();
startIntent();
}
}
~~~
代码很长,但总结下来就是,传不同的flag进去,定义不同的Dialog样式。上面代码为了方便只举了两个flag,其实在项目中你可以定义很多分类。并定义不同样式的Dialog。
以上代码可以传不同的数组进去,它都能正常的显示出来,复用性强。接下来我们来看看MainActivity如何调用。
~~~
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
cBtn = (customButton)super.findViewById(R.id.btn);
cBtn.setOutClickListener(new customButton.OutClickListener() {
@Override
public void popListener() {
Bundle b = new Bundle();
b.putInt("flag", 1);
Intent i = new Intent(MainActivity.this,CustomDialogActivity.class).putExtras(b);
startActivityForResult(i,1);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(resultCode == RESULT_OK){
Toast.makeText(MainActivity.this,data.getStringExtra("content"),Toast.LENGTH_SHORT).show();
}
}
~~~
好的,很简单,传入一个flag。而出来的年代数据是定义在布局文件string-array中的。
有的控件可能自定义进来很难,但这都是一个迭代的过程,再复杂的控件也是从简单控件入手的。之后会给出代码的下载地址。
[源码下载地址](https://github.com/reallin/CustomOutButton.git)
android新控件之toolbar,floatingActionButton,SnackBar,CollapsingToolbarLayout
最后更新于:2022-04-01 14:25:55
google 2015I/O大会推出了8个新的控件,继续推进了android5.0之后的Materia Design风格,个人觉得MD风格还是相当好看的,最近做项目用到了一部分控件,现在把几个控件用到同一个Demo中,结合demo来讲解它们,先看看效果图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-08_570771b4e8a35.jpg)
其中列表内容是通过xmlpull读取xml文件来显示的。限于篇幅,本次只讲几个。了解东西总是要从简单到复杂,那么我们就从简单的开始讲起。
### 一,snackBar
使用这些控件前先导入相关包,在android studio的build.gradle中添加
~~~
compile 'com.android.support:appcompat-v7:22.1.1'
~~~
Toast大家是再熟悉不过。那么如果会使用Toast,那么snackBar自然也会使用。那么snackBar长什么样呢,看下图
snackBar比Toast重量级一点,但又比Dialog轻。先看下snackBar的使用方法:
~~~
Snackbar snackbar = Snackbar.make(v, "弹出snackbar", Snackbar.LENGTH_LONG);
snackbar.show();
snackbar.setAction("取消", new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "SnackBar action", Toast.LENGTH_SHORT).show();
}
});
~~~
其实跟Toast使用没什么两样,只是多也一个按钮的触发。其中取消是按钮的名称。注意Snackbar.make()第一个参数不是context而是view,表示snackbar画在哪个view之上,所以第一个参数不能是scrollview,因为scrollview能滑动,snackbar不知放于哪个位置。
### 二.floatingActionButton
看以下图片就知道这是什么了。
这种悬浮按钮在Materia Design风格的app中很常见。可触发,可隐藏,用法也很简单,定义布局
~~~
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_discuss"
app:backgroundTintMode="multiply"
app:layout_anchorGravity="bottom|end|right"></android.support.design.widget.FloatingActionButton>
~~~
可以通过backgroundTint来改变改变背景颜色。anchorGravity确定button的放置位置。调用如下:
~~~
fab = (FloatingActionButton)findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } });
~~~
至此FloatingActionButton就完成了。
### 三、toolbar
个人觉得toolbar是一个非常实用的控件。之前使用ActionBar总会发现它自定义相当不方便,toolbar完美的解决了这个缺点,来看看toolbar的使用方法。
首先,将style文件中的AppTheme删除,不使用ActionBar风格,添加以下代码:
~~~
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<!-- Customize your theme here. -->
<!--导航栏底色-->
<item name="colorPrimary">@color/material_primary_color</item>
<!--状态栏底色-->
<item name="colorPrimaryDark">@color/material_primary_color_dark</item>
<!--导航栏上的标题颜色-->
<item name="android:textColorPrimary">@android:color/black</item>
<!--Activity窗口的颜色-->
<item name="android:windowBackground">@android:color/white</item>
<!--按钮选中或者点击获得焦点后的颜色-->
<item name="colorAccent">@color/accent_material_light</item>
<!--和 colorAccent相反,正常状态下按钮的颜色-->
<item name="colorControlNormal">@color/material_blue_grey_950</item>
<!--Button按钮正常状态颜色-->
<item name="colorButtonNormal">@color/button_material_light</item>
<!--EditText 输入框中字体的颜色-->
<item name="editTextColor">@android:color/white</item>
<item name="android:textColorHint">@color/hint_foreground_material_dark</item>
</style>
~~~
这些样式文件的值是可以修改的,改成你自己想要的颜色。
接下来添加toolbar的配置文件
~~~
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
/>
~~~
在activity中通过如下代码调用:
~~~
toolbar = (Toolbar) findViewById(R.id.toolbar);
//setActionBar(toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayShowTitleEnabled(false);
toolbar.setTitle("图书管理系统");
toolbar.setSubtitle("CSDC");
toolbar.setNavigationIcon(R.drawable.ic_list_black_24dp);
toolbar.setOnMenuItemClickListener(this);
~~~
setTitle设置主标题,setSubtitle设置次标题。setNavigationIcon设置Navigation点击的图标。toolbar还可以调用按钮,在menu_main文件中添加如下item:
~~~
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
<item
android:id="@+id/action_edit"
android:icon="@drawable/abc_ic_search_api_mtrl_alpha"
android:orderInCategory="80"
android:title="查找"
app:showAsAction="always" />
<item
android:id="@+id/action_share"
android:icon="@drawable/abc_ic_menu_share_mtrl_alpha"
android:orderInCategory="90"
android:title="分享"
app:showAsAction="always" />
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>
~~~
与Action一样,orderInCategory设置优先级,showAsAction设置是否显示在标题栏中。有四个属性,ifRoom,never,always,collapseActionView。
在Activity中复写onCreateOptionsMenu就可以显示菜单,可自行试试效果。
~~~
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
~~~
那么Toolbar如何自定义呢,不多说,贴出配置代码就知道了
~~~
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center_vertical"
android:text="标题"
android:textSize="18sp" />
</android.support.v7.widget.Toolbar>
~~~
就是把toolbar当成父布局来使用,其中的标题样式可自行摆放。
关于toolbar的用法就说这些,接下来看看CollaspingToolbarLayout的用法。
### 四、CollaspingToolbarLayout
这个可以理解为是一个用来包裹toolbar并在toolbar折叠时提供另一个切换视图。效果如下图:
这里切换视图实现了一个轮播效果,实现代码如下:
~~~
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:statusBarScrim="?attr/colorPrimary"
>
<LinearLayout
android:id="@+id/show_gallery"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.6"
android:orientation="vertical" >
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="198dp" />
--></LinearLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
/>
</android.support.design.widget.CollapsingToolbarLayout>
~~~
CollaspingToolbarLayout使用很简单,以上layout_collapseMode设置有两种模式:
1.pin:固定模式,折叠后最后固定在顶部。
2.parallax:折叠时有一个视差。
collapseParallxMultiplier设置的是视角差程度。
轮播(图片的左右切换)效果的实现如下:
~~~
mRecommendPager = (ViewPager)findViewById(R.id.pager);
mRecommendPager.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
PointF downP = new PointF();
PointF curP = new PointF();
int act = event.getAction();
if (act == MotionEvent.ACTION_DOWN
|| act == MotionEvent.ACTION_MOVE
|| act == MotionEvent.ACTION_UP) {
((ViewGroup) v).requestDisallowInterceptTouchEvent(true);
if (downP.x == curP.x && downP.y == curP.y) {
return false;
}
}
return false;
}
});
mRecommendPager.setAdapter(mRecommendAdapter);
LayoutInflater mLayoutInflater=LayoutInflater.from(this);
View view1=mLayoutInflater.inflate(R.layout.guide_one, null);
View view2=mLayoutInflater.inflate(R.layout.guide_two, null);
View view3=mLayoutInflater.inflate(R.layout.guide_end, null);
final ArrayList<View> views =new ArrayList<View>();
views.add(view1);
views.add(view2);
views.add(view3);
mRecommendAdapter = new RecommendAdapter(views,this);
mRecommendPager.setAdapter(mRecommendAdapter);
~~~
以上实现有一个小细节,就是为了ViewPager能监听到手势,用requestDiasllowInterceptTouchEvent(true)来防止手势被上层捕获。
这个不是本文的重点。
最后提供app源码的下载地址,通过完整的代码相信会了解的更快。
[
](https://github.com/reallin/MateriaDesignDemo)
[下载地载](https://github.com/reallin/MateriaDesignDemo)