Acitvity都被放再任务栈(TaskStack)中,不同的加载模式使得任务栈中的结构不同
四种加载模式分别是: - standard: 默认模式 - singleTop:
位于栈顶时唯一,如果栈顶已经是当前要创建的Activity,则不创建,否则创建新的Activity
- singleTask:
如果该Activity的实例不存在,则创建并获得栈顶位置;如果该Activity的实例已存在,会调用onNewIntent
方法,不会创建新的Activity实例,且原来已经存在的singTaskActivity上方的Activity均出栈,使得这个singTaskActivity获得栈顶位置
- singleInstance: 如果此Activity没有实例,它会创建一个新的任务栈;
如果任务栈中已经有此实例,会调用onNewIntent
方法,不会创建新的任务栈和实例;无论哪个Activity新建该singleInstanceActivity,只要已存在,都共享一个实例;由singleInstanceActivity创建的其他Activity,会尝试放在存在“亲属”关系的任务栈(taskAffinity)中,如果没有匹配的任务栈存在,则会创建新的任务栈存放被创建的Activity
配置加载模式的位置在AndroidManifest.xml中:
activity android:launchMode="singleTask"></activity> <
也可以在通过Intent启动Activity时指定FLAG,FLAG可以叠加使用
= new Intent(this, MyActivity.class);
Intent intent .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intentstartActivity(intent);
常用的FLAG有:
singleTask
相似,但单独使用会出现奇怪的现象,一般与FLAG_ACTIVITY_CLEAR_TOP
或FLAG_ACTIVITY_CLEAR_TASK
一起使用FLAG_ACTIVITY_NEW_TASK
一起使用时,可以达到和singleTask
一样的效果singleTop
相同FLAG_ACTIVITY_NEW_TASK
一起使用,每次启动都会创建新的TaskFLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
成对使用FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
的Intent请求启动这个堆栈时,还原点之上包括这个应用将会被清除onCreate()
->onAttachFragment()
->onContentChanged()
[->onRestart]->onStart()
[->onActivityResult()]->onRestoreInstanceState()
->onPostCreate()
->onResume()
->onPostResume()
->onAttachedToWindow()
->onCreateOptionsMenu()
->onPrepareOptionMenu()
->应用运行->onPause()
->onSaveInstanceState()
->onStop()
->onDestory()
当启动其他Activity时,调用onPause(),从其他Activity回来时,调用onResume() 当应用被放到后台时,调用onStop(),回到前台调用onRestart()->onStart();如果在后台时,由于内存不足等原因,进程被杀掉了,则回到前台时调的是onCreate(),而不会调onRestart() onSaveInstanceState会在onDestroy之前的任意时刻调,调用的时机不固定
onStart和onStop是根据是否可见调用的,onPause和onResume是根据是否在前台调用的
使用RadioGroup实现底部导航栏
<?xml version="1.0" encoding="utf-8"?>
RelativeLayout
< xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
FrameLayout
< android:id="@+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/tab_ll"/>
LinearLayout
< android:id="@+id/tab_ll"
android:layout_width="match_parent"
android:layout_height="match_parent">
RadioGroup
< android:paddingTop="5dp"
android:id="@+id/tab_bar"
android:background="@android:color/white"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center"
android:orientation="horizontal">
RadioButton
< android:id="@+id/tab_home"
android:gravity="center"
android:button="@null"
android:drawableTop="@drawable/selector_tab_home"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textColor="@drawable/selector_tab_color"
android:text="首页"/>
RadioButton
< android:id="@+id/tab_discover"
android:gravity="center"
android:button="@null"
android:drawableTop="@drawable/selector_tab_discover"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textColor="@drawable/selector_tab_color"
android:text="发现" />
RadioButton
< android:id="@+id/tab_personal"
android:gravity="center"
android:button="@null"
android:drawableTop="@drawable/selector_tab_personal"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textColor="@drawable/selector_tab_color"
android:text="个人中心"
/>RadioGroup>
</LinearLayout>
</RelativeLayout> </
android:button="@null"
或android:color=@android:color/transparent
可以去掉前面的小圆点
selector是状态选择器,可根据不同状态显示不同图片
<?xml version="1.0" encoding="utf-8" ?>
selector xmlns:android="http://schemas.android.com/apk/res/android">
<<!-- 没有焦点时的背景图片 -->
item android:state_window_focused="false" android:drawable="@drawable/pic1" />
<<!-- 非触摸模式下获得焦点并单击时的背景图片 -->
item android:state_focused="true" android:state_pressed="true" android:drawable= "@drawable/pic2" />
<<!-- 触摸模式下单击时的背景图片-->
item android:state_focused="false" android:state_pressed="true" android:drawable="@drawable/pic3" />
<<!--选中时的图片背景-->
item android:state_selected="true" android:drawable="@drawable/pic4" />
<<!--获得焦点时的图片背景-->
item android:state_focused="true" android:drawable="@drawable/pic5" />
<<!-- 默认时的背景图片-->
item android:drawable="@drawable/pic1" />
<selector> </
selector从上到下进行匹配,无匹配条件的item应放在最后
layer-list可以堆叠显示控件,如阴影效果:
<?xml version="1.0" encoding="utf-8"?>
layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="3dp"
< android:top="6dp">
shape>
<solid android:color="#b4b5b6"/>
<shape>
</item>
</<!-- 阴影层 -->
item android:bottom="6dp"
< android:right="3dp">
shape>
<solid android:color="#fff"/>
<shape>
</item>
</layer-list> </
叠加旋转:
<?xml version="1.0" encoding="utf-8"?>
layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<rotate android:fromDegrees="-10" android:pivotX="0" android:pivotY="0">
<bitmap android:src="@drawable/pic1"/>
<rotate>
</item>
</item>
<rotate android:fromDegrees="15" android:pivotX="0" android:pivotY="0">
<bitmap android:src="@drawable/pic2"/>
<rotate>
</item>
</item>
<rotate android:fromDegrees="40" android:pivotX="0" android:pivotY="0">
<bitmap android:src="@drawable/pic3"/>
<rotate>
</item>
</layer-list> </
在Activity中点击菜单键触发,用法:在Activity中重写onCreateOptionsMenu、onOptionsItemSelected
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuItem item = menu.add(1, 100, 1, "菜单一");
.setTitle("aaa");
item.setIcon(R.drawable.ic_launcher);
item.add(1, 101, 2, "菜单二");
menu.add(1, 102, 1, "菜单三");
menureturn true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case 100:
.makeText(MainActivity.this, "点击了菜单一", Toast.LENGTH_SHORT).show();
Toastbreak;
case 101:
//xxx
break;
case 102:
//xxx
break;
}
return super.onOptionsItemSelected(item);
}
View长按触发,用法:重写onCreateContextMenu、onContextItemSelected,并用activity.registerForContextMenu(view)
为view注册ContextMenu
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
showListView();
}
private void showListView(){
ListView listView = (ListView) findViewById(R.id.lv);
ArrayList<String> list=new ArrayList<>();
for(int i=0;i<5;i++){
.add("file"+(i+1));
list}
<String> adapter=new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,list);
ArrayAdapter.setAdapter(adapter);
listViewthis.registerForContextMenu(listView);//为listview注册上下文菜单
}
//创建菜单
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
//设置mune显示的内容
.setHeaderTitle("文件操作");
menu.setHeaderIcon(R.drawable.ic_launcher);
menu.add(1,1,1,"copy");
menu.add(1,2,1,"cut");
menu.add(1,3,1,"past");
menu.add(1,4,1,"cancel");
menu}
//响应菜单
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()){
case 1:
.makeText(this, "clicked copy",Toast.LENGTH_SHORT).show();
Toastbreak;
case 2:
//xxx
break;
case 3:
//xxx
break;
case 4:
//xxx
break;
}
return super.onContextItemSelected(item);
}
}
普通的确认对话框:
.Builder builder = new AlertDialog.Builder(this);
AlertDialog.setTitle("请做出选择").setIcon(R.drawable.ic_launcher)
builder.setMessage("标题")
.setPositiveButton("是", new OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
// TODO
}
}).setNegativeButton("否", new OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
// TODO
}
}).setNeutralButton("不知道", new OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
// TODO
}
});
.create().show(); builder
选项列表对话框:
final String[] items = new String[] { "北京", "上海", "广州", "深圳" };
.Builder builder = new AlertDialog.Builder(this);
AlertDialog.setIcon(R.drawable.ic_launcher).setTitle("标题")
builder.setItems(items, new OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// TODO
}
});
.create().show(); builder
单选列表对话框:
final String[] items = new String[] { "北京", "上海", "广州", "深圳" };
.Builder builder = new AlertDialog.Builder(this);
AlertDialog.setIcon(R.drawable.ic_launcher).setTitle("标题")
builder.setSingleChoiceItems(items, 0, new OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// TODO
}
});
.create().show(); builder
多选列表对话框:
final String[] items = new String[] { "北京", "上海", "广州", "深圳" };
.Builder builder = new AlertDialog.Builder(this);
AlertDialog.setIcon(R.drawable.ic_launcher)
builder.setTitle("标题")
.setMultiChoiceItems(items,
new boolean[] { true, true, false, false, false },
new OnMultiChoiceClickListener() {
public void onClick(DialogInterface dialog,int which, boolean isChecked) {
if (isChecked) {
.makeText(MainActivity.this,items[which], 0).show();
Toast}
}
});
.create().show(); builder
自定义列表项:
final String[] items = new String[] { "北京", "上海", "广州", "深圳" };
.Builder builder = new AlertDialog.Builder(this);
AlertDialog.setTitle("标题")
builder.setIcon(R.drawable.ic_launcher)
.setAdapter(
new ArrayAdapter<String>(MainActivity.this,
.layout.item, R.id.tv, items),
Rnew OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// TODO
.makeText(MainActivity.this, items[which], 0).show();
Toast}
});
.create().show(); builder
自定义View列表:
.Builder builder = new AlertDialog.Builder(this);
AlertDialogView view2 = View.inflate(MainActivity.this, R.layout.login, null);
final EditText username = (EditText) view2.findViewById(R.id.username);
final EditText password = (EditText) view2.findViewById(R.id.password);
final Button button = (Button) view2.findViewById(R.id.btn_login);
.setTitle("登录").setIcon(R.drawable.ic_launcher)
builder.setView(view2);
final AlertDialog alertDialog = builder.create();
.setOnClickListener(new View.OnClickListener() {
buttonpublic void onClick(View v) {
String uname = username.getText().toString().trim();
String psd = password.getText().toString().trim();
if (uname.equals("username") && psd.equals("password")) {
.makeText(MainActivity.this, "登录成功", 0).show();
Toast}
.makeText(MainActivity.this, "登录失败", 0).show();
Toast.dismiss();// 对话框消失
alertDialog}
});
.show(); alertDialog
//获取当前年月日、时间
Calendar calendar = Calendar.getInstance();
int year = calendar.get(calendar.YEAR);
int monthOfYear = calendar.get(calendar.MONTH);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
new DatePickerDialog(this, new OnDateSetListener() {
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
// TODO
}
}, year, monthOfYear, dayOfMonth).show();
new TimePickerDialog(this, new OnTimeSetListener() {
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
// TODO
}
}, hourOfDay, minute, true).show();
常用属性有: - listSelector:数据项在选中、按下等不同状态时的Drawable - android:divider:使用一个Drawable或者color设置数据项之间的间隔样式; - android:dividerHeight:设置数据项之间的间隔距离; - android:entries:设置一个资源Id用于填充ListView的数据项; - android:footerDividersEnabled:设定列表表尾是否显示分割线,如果有表尾的话; - android:headerDividerEnabled:设定列表表头是否显示分割线,如果有表头的话;
常用方法有: - void addFooterView(View v):添加表尾View视图; - boolean removeFooterView(View v):移除一个表尾View视图; - void addHeaderView(View v):添加一个表头View视图; - boolean removeHeaderView(View v):移除一个表头View视图; - void setEmptyView(View v):设置数据项为0时的空数据视图 - ListAdapter getAdapter():获取当前绑定的ListAdapter适配器; - void setAdapter(ListAdapter adapter):设置一个ListAdapter适配器到当前ListView中; - void setSelection(int posotion):设定当前选中项; - long[] getCheckItemIds():获取当前选中项;
常用有四种Adapter:ArrayAdapter、SimpleAdapter、SimpleCursorAdapter、BaseAdapter
Adapter不仅可以用在ListView中,还能用在Spinner、ViewPager等其他控件中,虽然用的Adapter有所不同,但其设计思想是一致的
SimpleAdapter:
ArrayList<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();
for(int i = 0; i < 10; i++){
HashMap<String, Object> map = new HashMap<String, Object>();
.put("ItemImage", R.drawable.icon);//加入图片
map.put("ItemTitle", "第"+i+"行");
map.put("ItemText", "这是第"+i+"行");
map.add(map);
listItem}
= new SimpleAdapter( MainActicity.this, listItem, R.layout.item, new String[]{"ItemImage","ItemTitle", "ItemText"}, new int[] {R.id.ItemImage,R.id.ItemTitle,R.id.ItemText} );
SimpleAdapter mSimpleAdapter
.setChoiceMode(ListView.CHOICE_MODE_SINGLE);//设置选择模式为单选
listview.setAdapter(mSimpleAdapter);
listview.setOnItemClickListener(new OnItemClickListener(){
lvpublic void onItemClick(ListView parent, View view, int position, long id) {
// TODO
}
});
BaseAdapter
public class MyAdapter extends BaseAdapter {
private List<Student> stuList;
private LayoutInflater inflater;
public MyAdapter(List<Student> stuList,Context context) {
this.stuList = stuList;
this.inflater=LayoutInflater.from(context);
}
@Override
public int getCount() {
return stuList==null?0:stuList.size();
}
@Override
public Student getItem(int position) {
return stuList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
//listview只加载 一页可显示的item数+1 个的item,每次滚动只是把item的内容改了,而不会重新inflate一个item,convertView就是复用的组件,convertView只有第一次加载listview的时候才inflate可以节省内存
if(convertView == null){
= inflater.inflate(R.layout.layout_student_item,null);
convertView = new ViewHolder();
ViewHolder viewHolder .image_photo = (ImageView) view.findViewById(R.id.image_photo);
viewHolder.tv_name = (TextView) view.findViewById(R.id.name);
viewHolder.tv_age = (TextView) view.findViewById(R.id.age);
viewHolder.setTag(viewHolder);
convertView= convertView;
view }else{
= convertView;
view }
=getItem(position);
Student student= view.getTag();
ViewHolder mViewHolder .image_photo.setImageResource(student.getPhoto());
mViewHolder.tv_name.setText(student.getName());
mViewHolder.tv_age.setText(String.valueOf(student.getAge()));
mViewHolderreturn view;
}
class ViewHolder{
ImageView image_photo;
;
TextView tv_name;
TextView tv_age}
}
更新ListView的数据:
//改变数据集(stuList),然后调
.notifyDataSetChanged(); adapter
xml
android.support.v4.view.ViewPager
< android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent">
android.support.v4.view.ViewPager> </
JAVA代码(自定义PagerAdapter,并对ViewPager设置自己的PagerAdapter)
private void setupViewpager() {
List<int> list = new ArrayList<>();
for (int i = 0; i < 8; i++) {
int id = getResources().getIdentifier("img_" + i, "mipmap", getPackageName());
.add(id);
list}
= (ViewPager) findViewById(R.id.viewpager);
ViewPager viewpager //设置PagerAdapter,如果与Fragment一起使用,使用的Adapter就是FragmentPagerAdapter或FragmentStatePagerAdapter
.setAdapter(new MyPagerAdapter(this,list));
viewpager//设置翻页动画
.setPageTransformer(false,new ViewPager.PageTransformer(){
viewpagerpublic void transformPage(View view, float position) {
//根据position对view进行动画设置
}
});
//设置翻页监听
.addOnPageChangeListener(new ViewPager.OnPageChangeListener(){
viewpagerpublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
public void onPageSelected(int position) {}
public void onPageScrollStateChanged(int state) {}
});
}
//自定义PagerAdapter
public class MyPagerAdapter extends PagerAdapter{
private Context mContext;
private List<String> mData;
public MyPagerAdapter(Context context, List<String> list) {
= context;
mContext = list;
mData }
@Override
public int getCount() {
//如果想无限滑动,则返回一个较大的数(如:Integer.MAX_VALUE),然后通过viewpager.setCurrentItem计算设置起始页为中间的值(Integer.MAX_VALUE/2 - (Integer.MAX_VALUE/2 % mData.size())),但这时后面instantiateItem的position也要取模才是真实的position
return mData.size();
}
//相当于ListView的getView,只不过还要将view加到viewgroup中,因为ViewPager不存在组件复用的情况,所以使不使用ViewHolder其实无所谓
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = View.inflate(mContext, R.layout.item,null);
ImageView img = (ImageView)view.findViewById(R.id.img);
.setImageResource(list.get(position));
img.addView(view);
containerreturn view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
.removeView((View)object);
container}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
}
ViewPager其实是通过view.scrollTo
和view.scrollBy
实现的,这两个函数可以移动所有的子view,scrollTo直接指定坐标,scrollBy指定偏移量,要注意的是,该偏移量向右为负,向左为正,向下为负,向上为正,是和transition时相反的
float lastX;
float lastY;
protected boolean onTouchEvent(MotionEvent event){
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//getX得到的是相对父布局原点的X,getRawX得到的是相对于屏幕左上方的X,Y同理
= event.getX();
lastX = event.getY();
lastY break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
//这里计算的DX、DY是transition时的(滑动和滚动是相反的,一个是背景(绿幕)在动,一个是手机屏幕在动),在scrollBy中要相对于起始坐标取反
int DX = (int)(endX - lastX);
int DY = (int)(endY - lastY);
//如果只能水平滑动,第二个参数就是getScrollY,getScrollY是相对于父布局的,由于每个item都会有一个viewgroup包裹,view放在父布局原点,一般getScrollY都是0,但如果view不是放在父布局原点,getScrollY就不是0
//当view不是放在父布局原点时,就不能写成-DY了,必须用getScrollY() - DY
scrollTo(getScrollX() - DX, getScrollY() - DY);
= event.getX();
lastX = event.getY();
lastY break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}
同时ViewPager还有回弹,是通过Scroller
实现的,原理时从startScroll开始,通过指定的起始和结束位置以及时间,分别计算X方向和Y方向的速度,然后每调一次invalidate,就根据调用的时长(invalidate
->draw
->ondraw
->computeScroll
->invalidate
)滑动对应的距离(getCurrX、getCurrY,以坐标形式给出),直到滑动到对应位置(要在computeScroll
判断,如果没有完成滑动,就调invalidate
开始下一个周期)
;
Scroller mScrollerprotected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
= new Scroller(MyActivity.this);
mScroller .setOnClickListener(new OnClickListener(){
buttonpublic void onClick(View view) {
//获取屏幕宽度
= new DisplayMetrics();
DisplayMetrics metric .getWindowManager().getDefaultDisplay().getRealMetrics(metric);
contextint width = metric.widthPixels;
//startScroll(int startX,int startY,int dx,int dy,int duration);
//从(0, 0)开始,向右滑动一个屏幕,总共500毫秒完成
.startScroll(0,0, -width, 0, 500);
mScrollerinvalidate();
}
});
}
//调用invalidate或postInvalidate时,除了调用onDraw外,还会调computeScroll
public void computeScroll(){
if (mScroller.computeScrollOffset()) {
//还没有完成滑动,继续滑动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
super.computeScroll();
}
手势操作还可以通过GestureDetector
或ViewDragHelper
实现
private GestureDetector mDetector;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//OnGestureListener也可以用SimpleOnGestureListener,其实就是一个把抽象方法改成空方法的OnGestureListener,不用写那么多个用不到的函数而已
= new GestureDetector(MyActivity.this,new GestureDetector.OnGestureListener(){
mDetector public boolean onDown(MotionEvent e){return true;}
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY){return true;}
public void onLongPress(MotionEvent e){}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
scrollBy((int)(getScrollX()-distanceX), (int)(getScrollY()-distanceY));
return true;
}
public void onShowPress(MotionEvent e){}
public boolean onSingleTapUp(MotionEvent e){return true;}
});
}
//在onTouchEvent调GestureDetector的onTouchEvent,将事件交由GestureDetector处理
@Override
public boolean onTouchEvent(MotionEvent event){
this.mDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}
有时比如向左滑出菜单会和点击事件冲突(滑出菜单由item实现,点击由子view实现,而且可能有多个按钮,就有多个点击事件),这时可以通过拦截事件来解决,这里是item的代码
int menuwidth;//侧滑菜单的宽度,在onCreate时已赋值
;//系统提供的计算回弹坐标的类,在onCreate时已赋值
Scroller mScrollerfloat downX;
float downY;
float lastX;
float lastY;
public boolean onInterceptTouchEvent(Motion event){
//intercept为true表示拦截事件,让事件由自己处理(拦截后会调用当前的onTouchEvent,在onTouchEvent中写滑出菜单的代码),为false表示不拦截,让item内每个按钮处理对应的点击事件
boolean intercept = false;
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
= event.getX();
downX = event.getY();
downY break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
= endX - downX;
floast DX = endY - downY;
floast DY if(Math.abs(DX) > 8){//水平滑动超过8px,则为水平滑动
= true;
intercept }
break;
case MotionEvent.ACTION_UP:
break;
}
return intercept;
}
/*
作用:
1、让每个item在水平方向跟随用户拖动而移动
2、在ACTION_UP时,判断是要弹出菜单还是回弹菜单
*/
protected boolean onTouchEvent(MotionEvent event){
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
= event.getX();
lastX = event.getY();
lastY break;
case MotionEvent.ACTION_MOVE:
float endX = event.getX();
float endY = event.getY();
int DX = (int)(endX - lastX);
int DY = (int)(endY - lastY);
int toScrollX = (int)(getScrollX() - DX);
//屏蔽非法值,这里view的原点在父布局原点,getScrollX、getScrollY可以理解为view坐标(或偏移量,因为起点是(0,0),移动多少偏移量也是多少,但要记住,这个偏移量是和transition相同的,和srcoll相反的),这里限制坐标在0~menuwidth之间
if(toScrollX < 0){
= 0;
toScrollX }else if(toScrollX > menuwidth){
= toScrollX;
toScrollX }
scrollTo(toScrollX, getScrollY());
= event.getX();
lastX = event.getY();
lastY break;
case MotionEvent.ACTION_UP:
//判断是滑出菜单还是回弹菜单
//当view原点位于父布局的原点位置时,getScrollX、getScrollX可以理解为偏移量,这里view原点在父布局原点
if(getScrollX() < menuwidth/2){
//回弹菜单
closeMenu();
}else{
//滑出菜单
openMenu();
}
break;
}
return super.onTouchEvent(event);
}
/*
这里滑出和回弹封装成方法的好处是:
1、当以后加到ListView中,ListView向下滑动,item要自动回弹,这时可以设ListView监听,在ListView滚动时调回弹的方法
2、限制只能同时打开一个item菜单,当openMenu时,自定义回调接口通知用户打开了菜单,这时就要关闭其他的item菜单
*/
//滑出菜单
public void openMenu(){
.startScroll(getScrollX(), getScrollY(), menuwidth - getScrollX(), getScrollY())
mScrollerinvalidate();
}
//回弹菜单(水平方向回弹,竖直方向不变)
public void closeMenu(){
.startScroll(getScrollX(), getScrollY(), - getScrollX(), getScrollY())
mScrollerinvalidate();
}
//由于用到了Scroller,需重写此方法
public void computeScroll(){
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
super.computeScroll();
}
要自动滑动ViewPager,可以用handler发延时消息,这里只提供思路,不再列出具体实现
使用TabLayout前需在build.gradle中添加依赖
compile 'com.android.support:design:25.2.0'
xml布局
<?xml version="1.0" encoding="utf-8"?>
LinearLayout
< xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
>android.support.design.widget.TabLayout
< android:id="@+id/tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
app:tabGravity="fill"
app:tabIndicatorColor="@color/toolBarColor"
app:tabMode="fixed"
app:tabSelectedTextColor="@color/toolBarColor"
app:tabTextColor="#000000"
/>android.support.v4.view.ViewPager
< android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:background="@android:color/white"/>
LinearLayout> </
在ViewPager中使用Fragment需用FragmentPagerAdapter
public class MyPagerAdapter extends FragmentPagerAdapter {
private Context context;
private List<Fragment> fragmentList;
private List<String> list_Title;//TabLayout中的title
public MyPagerAdapter(FragmentManager fm,Context context,List<Fragment> fragmentList,List<String> list_Title) {
super(fm);
this.context = context;
this.fragmentList = fragmentList;
this.list_Title = list_Title;
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return list_Title.size();
}
//此方法用来显示tab上的标题
@Override
public CharSequence getPageTitle(int position) {
return list_Title.get(position);
}
关联TabLayout和ViewPager
= new ArrayList<>();
fragmentList = new ArrayList<>();
list_Title .add(new MyFragment());
fragmentList.add(new MyFragment());
fragmentList.add("one");
list_Title.add("two");
list_Title.setAdapter(new MyPagerAdapter(getSupportFragmentManager(), MyActivity.this, fragmentList, list_Title));
viewpager
.setupWithViewPager(viewpager);//此方法就是让tablayout和ViewPager联动
tablayout.setTabMode(TabLayout.MODE_SCROLLABLE);//如果标签很多,需要设置可滑动 tablayout
使用前需在build.gradle中添加依赖
implementation 'com.android.support:recyclerview-v7:27.0.2'
在xml中使用
android.support.v7.widget.RecyclerView
< android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
在JAVA中配置
= findViewById(R.id.recyclerView);
mRecyclerView //设置LayoutManager,可以通过LayoutManager设置显示为线性、网格(GridLayoutManager)、瀑布流(StaggeredGridLayoutManager,item高度不一致时使用)等
.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
mRecyclerView//设置添加或删除item时的动画,这里使用默认动画
.setItemAnimator(new DefaultItemAnimator());
mRecyclerView//添加分割线,添加多个时叠加显示
//ItemDecoration内置里一共有三个类继承该类,分别是 DividerItemDecoration(普通分割线,可以实现section(联系人列表中的按首字母拼音分组)、StickyHeader等效果),ItemTouchHelper(可以实现拖拽和移动,侧滑删除等效果),FastScroller(包权限,不开放给外部使用)
.addItemDecoration(new DividerItemDecoration(MainActivity.this, RecyclerView.ItemDecoration.HORIZONTAL));
recyclerView//设置适配器
= new MyRecyclerViewAdapter(list);
mAdapter .setAdapter(mAdapter); mRecyclerView
StickyHeader可参考 https://github.com/timehop/sticky-headers-recyclerview
Adapter与ListView类似,但ViewHolder要使用RecyclerView.ViewHolder
public class MyRecyclerViewAdapterextends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private List<String> list;//要显示的数据
public MyAdapter(List<String> list) {
this.list = list;
}
@Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
.ViewHolder viewHolder = new MyAdapter.ViewHolder(view);
MyAdapterreturn viewHolder;
}
@Override
public void onBindViewHolder(MyAdapter.ViewHolder holder, int position) {
.mText.setText(list.get(position));
holder}
@Override
public int getItemCount() {
return list.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
;
TextView mTextViewHolder(View itemView) {
super(itemView);
= itemView.findViewById(R.id.item_tx);
mText }
}
}
FloatButton继承自ImageButton,基本用法和Button一样
使用前先在build.gradle中添加依赖
compile 'com.android.support:design:22.2.0'
xml配置
android.support.design.widget.FloatingActionButton
< android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|bottom"
android:src="@mipmap/floatbutton"
android:layout_margin="@dimen/fab_margin"
app:fabSize="mini"
app:backgroundTint="#ff87ffeb"
app:borderWidth="0dp"
app:rippleColor="#33728dff"
app:elevation="6dp"
app:pressedTranslationZ="12dp" />
app:borderWidth
可以解决在5.x设备上没有阴影的问题,android:layout_margin
的作用是防止设置了borderWidth后出现矩形边框,app:backgroundTint
是按钮的背景颜色,app:rippleColor
是点击的边缘阴影颜色,app:elevation
是边缘阴影的宽度,app:pressedTranslationZ
是点击时边缘阴影的宽度
values.xml
dimen name="fab_margin">0dp</dimen> <
values-v21.xml
dimen name="fab_margin">16dp</dimen> <
AppWidget
是桌面小部件,比如常见的天气预报、时钟等就是AppWidget
使用Android
Studio帮我们创建一个AppWidget
,这时生成了三个文件
MyAppWidget.java
public class MyAppWidget extends AppWidgetProvider {
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
CharSequence widgetText = context.getString(R.string.appwidget_text);
= new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
RemoteViews views .setTextViewText(R.id.appwidget_text, widgetText);
views.updateAppWidget(appWidgetId, views);
appWidgetManager}
//当小部件被添加时或者每次小部件更新时都会调用一次该方法
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
//当小部件第一次被添加到桌面时回调该方法,可添加多次,但只在第一次调用
@Override
public void onEnabled(Context context) { }
//当最后一个该类型的小部件从桌面移除时调用
@Override
public void onDisabled(Context context) { }
}
首先创建了一个RemoteViews
(远程view,它在其它进程中显示,却可以在另一个进程中更新),用于显示我们的AppWidget
,然后通过RemoteViews
的setXXX
方法对AppWidget
的内容进行更新,最后通过updateAppWidget
执行更新
这里的my_app_widget
是一个普通的布局文件,但是因为它只能通过RemoteViews
的setXXX
方法进行更新,所以能更新的view类型是有限的,它只能更新基本的系统控件,我们如果在AppWidget
中使用了自定义控件,会无法更新
my_app_widget.xml
RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
< android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#09C"
android:padding="@dimen/widget_margin">
TextView
< android:id="@+id/appwidget_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_margin="8dp"
android:background="#09C"
android:contentDescription="@string/appwidget_text"
android:text="@string/appwidget_text"
android:textColor="#ffffff"
android:textSize="24sp"
android:textStyle="bold|italic" />
RelativeLayout> </
AppWidget
是通过BroadCastReceiver
的形式进行控制的,我们继承的AppWidgetProvider
其实继承自
BroadCastReceiver
,所以在AndroidManifest.xml
中会生成receiver
AndroidManifest.xml
receiver android:name=".MyAppWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<intent-filter>
</
meta-data
< android:name="android.appwidget.provider"
android:resource="@xml/my_app_widget_info" />
receiver> </
其中action是AppWidget
的固定写法,而AppWidget
的配置是通过meta-data
中指向的文件设置的
xml文件夹下的my_app_widget_info.xml指定了AppWidget
的最小宽高,可调整大小的方向,更新时间间隔等
<?xml version="1.0" encoding="utf-8"?>
appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
< android:initialKeyguardLayout="@layout/my_app_widget"
android:initialLayout="@layout/my_app_widget"
android:minWidth="110dp"
android:minHeight="40dp"
android:previewImage="@drawable/example_appwidget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"></appwidget-provider>
如果要实现点击AppWidget
打开APP,这时一般不用Intent
,而是用PendingIntent
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
= new Intent(context, MainActivity.class);
Intent intent = PendingIntent.getActivity(context, 0, intent,0);
PendingIntent pendingIntent = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
RemoteViews view .setOnClickPendingIntent(R.drawable.ic_launcher_background, pendingIntent);
view.updateAppWidget(R.layout.my_app_widget,view);
appWidgetManager}
长按应用图标显示应用快捷键菜单(Shortcuts),Android7.0加入的功能
在/res/xml
中创建一个xml文件,这里我们命名为shortcuts.xml
shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
< android:shortcutId="compose"
android:enabled="true"
android:icon="@drawable/compose_icon"
android:shortcutShortLabel="@string/compose_shortcut_short_label1"
android:shortcutLongLabel="@string/compose_shortcut_long_label1"
android:shortcutDisabledMessage="@string/compose_disabled_message1">
intent
< android:action="android.intent.action.VIEW"
android:targetPackage="com.example.myapplication"
android:targetClass="com.example.myapplication.ComposeActivity" />
categories android:name="android.shortcut.conversation" />
<shortcut>
</<!-- Specify more shortcuts here. -->
shortcuts> </
然后在AndroidManifest.xml
中配置activity
的meta-data
activity android:name="Main">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<intent-filter>
</
meta-data android:name="android.app.shortcuts"
< android:resource="@xml/shortcuts" />
activity> </
要注意的是,能配置shortcuts
的activity
必须是android.intent.action.MAIN
和android.intent.category.LAUNCHER
静态配置的shortcuts
无法设置Intent
的flag,flag默认为FLAG_ACTIVITY_NEW_TASK
和FLAG_ACTIVITY_CLEAR_TASK
,也就是如果当前Activity已经启动,通过shortcuts
启动应用会把当前Activity销毁
通过ShortcutManager
配置
添加:
= getSystemService(ShortcutManager.class);
ShortcutManager shortcutManager
= new ShortcutInfo.Builder(context, "id1")
ShortcutInfo shortcut .setShortLabel("Website")
.setLongLabel("Open the website")
.setIcon(Icon.createWithResource(context, R.drawable.icon_website))
.setIntent(new Intent(Intent.ACTION_VIEW,
.parse("https://www.mysite.example.com/")))
Uri.build();
.setDynamicShortcuts(Arrays.asList(shortcut)); shortcutManager
更新:
List<ShortcutInfo> infoList = shortcutManager.getPinnedShortcuts();
//修改infoList
//...
.updateShortcuts(infoList); shortcutManager
删除:
= getSystemService(ShortcutManager.class);
ShortcutManager shortcutManager List<ShortcutInfo> infoList = shortcutManager.getDynamicShortcuts();
List<String> idList = new ArrayList<>();
for(ShortcutInfo s : infoList){
.add(s.getId());
idList}
.disableShortcuts(idList, "已禁用");
shortcutManager.removeDynamicShortcuts(idList); shortcutManager
给Shortcuts条目生成应用图标放到桌面,Android8.0加入的功能
= getSystemService(ShortcutManager.class);
ShortcutManager shortcutManager
if (shortcutManager.isRequestPinShortcutSupported()) {
//对于设置为enabled的已存在的shortcut,可以直接通过id获取
//但如果是新建一个shortcut,就要指定id、shortLabel、intent
//这里假设id为my-shortcut的shortcut已存在且为enabled
= new ShortcutInfo.Builder(this, "my-shortcut").build();
ShortcutInfo pinShortcutInfo
//当用户尝试给shortcut创建桌面快捷方式时,我们要通过程序检测是否运行用户创建,这时就要用回调
//对应的context需要重写createShortcutResultIntent(),并在该函数中处理回调
= shortcutManager.createShortcutResultIntent(pinShortcutInfo);
Intent pinnedShortcutCallbackIntent
= PendingIntent.getActivity(this, 0, pinnedShortcutCallbackIntent, 0);
PendingIntent successCallback
.requestPinShortcut(pinShortcutInfo, successCallback.getIntentSender());
shortcutManager}
对于Pinned Shortcuts
,只能由用户删除,我们没有权限删除,但我们可以禁用
= getSystemService(ShortcutManager.class);
ShortcutManager shortcutManager
List<ShortcutInfo> infoList = shortcutManager.getPinnedShortcuts();
List<String> idList = new ArrayList<>();
for (ShortcutInfo info : infoList) {
.add(info.getId());
idList}
.disableShortcuts(idList, "已失效");
shortcutManager//虽然用了remove,但实际上只是禁用了
.removeDynamicShortcuts(idList); shortcutManager
布局的通用属性: - layout_margin:外边距 - layout_padding:内边距 - layout_width/height:宽高 - gravity:组件内容的对齐方式 - layout_gravity:自身相对于父元素的布局
常用的属性有
1、相对于父控件 - android:layout_alignParentTop 控件的顶部与父控件的顶部对齐; - android:layout_alignParentBottom 控件的底部与父控件的底部对齐; - android:layout_alignParentLeft 控件的左部与父控件的左部对齐; - android:layout_alignParentRight 控件的右部与父控件的右部对齐; - layout_alignParentStart 控件与父控件的开始位置对齐 - layout_alignParentStop 控件与父控件的结束位置对齐
2、相对给定ID控件 - layout_above 控件的底部置于给定ID的控件之上; - android:layout_below 控件的底部置于给定ID的控件之下; - android:layout_toLeftOf 控件的右边缘与给定ID的控件左边缘对齐; - android:layout_toRightOf 控件的左边缘与给定ID的控件右边缘对齐; - android:layout_alignBaseline 控件的baseline与给定ID的baseline对齐; - android:layout_alignTop 控件的顶部边缘与给定ID的顶部边缘对齐; - android:layout_alignBottom 控件的底部边缘与给定ID的底部边缘对齐; - android:layout_alignLeft 控件的左边缘与给定ID的左边缘对齐; - android:layout_alignRight 控件的右边缘与给定ID的右边缘对齐; - layout_alignStart 控件与给定ID的开始位置对齐 - layout_alignStop 控件与给定ID的结束位置对齐
3、居中 - android:layout_centerHorizontal 水平居中; - android:layout_centerVertical 垂直居中; - android:layout_centerInParent 父控件的中央;
从屏幕左上角按照层次堆叠方式布局,后面的控件覆盖前面的控件
通过android:layout_x
和android:layout_y
,设置坐标,屏幕左上角为坐标(0,0)
AbsoluteLayout的替代品,对于Android Studio的拖拽控件有较好的支持,使用前需添加依赖
dependencies {
compile 'com.android.support.constraint:constraint-layout:1.0.2'
}
由多个TableRow组成,一个TableRow由若干个view组成;TableLayout常用属性有 - android:shrinkColumns 设置可收缩的列,内容过多就收缩显示到第二行;; - android:stretchColumns 设置可伸展的列,将空白区域填充满整个列; - android:collapseColumns 设置要隐藏的列;
子控件常用属性: - android:layout_column 第几列; - android:layout_span 占据列数;
网格布局,常用属性有 - android:rowCount 设置行数 - android:columnCount 设置列数 - android:layout_row 组件在第几行 - android:layout_column 组件在第几列 - android:layout_rowSpan 组件横跨几行 - android:layout_columnSpan 组件横跨几列 - android:layout_gravity = “fill” 组件横跨几行或列后,需要加上填充属性
抽屉布局,用于侧滑菜单(该布局在v4包:android.support.v4.widget.DrawerLayout
),里面一般使用NavigationView
来实现菜单项
setDrawerListener(DrawerLayout.DrawerListener)
,一般用其实现类ActionBarDrawerToggle
或DrawerLayout.SimpleDrawerListener
private class DrawerMenuToggle extends ActionBarDrawerToggle{
public DrawerMenuToggle(Activity activity, DrawerLayout drawerLayout, int drawerImageRes, int openDrawerContentDescRes, int closeDrawerContentDescRes) {
super(activity, drawerLayout, drawerImageRes, openDrawerContentDescRes,closeDrawerContentDescRes);
}
//当侧滑菜单达到完全关闭的状态时,回调这个方法
public void onDrawerClosed(View view) {
super.onDrawerClosed(view);
invalidateOptionsMenu();
}
//当侧滑菜单完全打开时,这个方法被回调
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
invalidateOptionsMenu();
}
}
;
ActionBarDrawerToggle mDrawerToggle;
DrawerLayout mDrawerLayoutListView menuDrawer;//这里使用ListView显示菜单项
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
.syncState();
mDrawerToggle}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
.onConfigurationChanged(newConfig);
mDrawerToggle}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (mDrawerToggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}
//每次调用 invalidateOptionsMenu() ,下面的这个方法就会被回调
//之前在侧滑菜单的状态监听器中打开和关闭事件都调用了invalidateOptionsMenu()方法
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean drawerOpen = mDrawerLayout.isDrawerOpen(menuDrawer);
//对其他控件,如ActionBar做相应处理
return super.onPrepareOptionsMenu(menu);
}
//当按下返回功能键的时候,不是直接对Activity进行弹栈,而是先将菜单视图关闭
@Override
public void onBackPressed() {
boolean drawerState = mDrawerLayout.isDrawerOpen(menuDrawer);
if (drawerState) {
.closeDrawers();
mDrawerLayoutreturn;
}
super.onBackPressed();
}
也是v4包中的布局,和DrawerLayout相似,但它可以指定两个子布局,第一个子布局就是侧滑菜单,第二个子布局是正常视图,下面是一个SlidingPaneLayout + ViewPager + Fragment + RadioGroup的例子
<?xml version="1.0" encoding="utf-8"?>
android.support.v4.widget.SlidingPaneLayout
< 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">
RelativeLayout
< android:layout_width="250dp"
android:layout_height="match_parent">
TextView
< android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="这是侧滑菜单"
android:textSize="30sp"/>
RelativeLayout>
</RelativeLayout
< android:layout_width="match_parent"
android:layout_height="match_parent">
RadioGroup
< android:layout_width="match_parent"
android:layout_height="60dp"
android:id="@+id/rg"
android:orientation="horizontal"
android:layout_alignParentBottom="true">
RadioButton
< style="@style/bt"
android:text="Red"
android:checked="true"
android:id="@+id/red_bt" />
RadioButton
< style="@style/bt"
android:text="Green"
android:id="@+id/green_bt"/>
RadioButton
< style="@style/bt"
android:text="Blue"
android:id="@+id/blue_bt"/>
RadioGroup>
</android.support.v4.view.ViewPager
< android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/rg"
android:id="@+id/frag_vp"/>
RelativeLayout>
</android.support.v4.widget.SlidingPaneLayout> </
编辑SharePreference
/*
MODE_APPEND: 追加方式存储
MODE_PRIVATE: 私有方式存储,其他应用无法访问
MODE_WORLD_READABLE: 表示当前文件可以被其他应用读取
MODE_WORLD_WRITEABLE: 表示当前文件可以被其他应用写入
*/
= getSharedPreferences("filename", Context.MODE_PRIVATE);
SharedPreferences sharedPreferences
= sharedPreferences.edit();
Editor editor .putString("key", "value");
editor.putInt("num", 1234);
editor.commit();//提交
editor
.getString("key", "");//第二个参数为如果key字段不存在时的默认返回值 sharedPreferences
删除SharedPreferences产生的文件
File file= new File("/data/data/"+getPackageName().toString()+"/shared_prefs","filename.xml");
if(file.exists()){
.delete();
file}
访问其他应用的SharedPreferences(要求要访问的应用的Preference创建时指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE权限)
//创建其他应用的Context,通过该Context获取SharedPreferences
Context otherAppsContext = createPackageContext("com.myapp.app", Context.CONTEXT_IGNORE_SECURITY);
= otherAppsContext.getSharedPreferences("filename", Context.MODE_WORLD_READABLE);
SharedPreferences sharedPreferences
//也可以通过直接读取xml来获取其他应用的SharedPreferences
File xmlFile = new File(“/data/data/com.otherapp.app/shared_prefs/filename.xml”);
内部存储:
//路径为/data/data/package_name/files
FileOutputStream fos = openFileOutput(fileName,MODE_PRIVATE);
FileInputStream fin = openFileInput(fileName);
String filePath = getFilesDir()+File.separator+"filename.txt"
//路径为/data/data/package_name/cache,若设备内部存储空间不足时,系统会自动删掉该文件夹下的文件
String cachePath = getCacheDir()+File.separator+"cachefile"
外部存储:
用到的权限有
uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> <
java代码:
//路径为/mnt/sdcard
Environment.getExternalStorageDirectory();
//路径为/mnt/sdcard/Android/data/package_name/files,app被删除时,该文件夹也一并清空;如果参数非null,则得到的是files下的子文件夹的路径
getExternalFilesDir(null);
//路径为/mnt/sdcard/Android/data/package_name/cache
getExternalCacheDir();
//一般要先判断是否有sd卡
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
//SD卡已装入
}
继承SQLiteOpenHelper,重写onCreate、onUpgrade,然后通过自己的SQLiteOpenHelper获取SQLiteDatabase,进行数据库操作
public class MyDataBaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table Book("
+ "id integer primary key autoincrement,"
+ "author text,"
+ "price real,"
+ "pages integer,"
+ "name text)";
public static final String CREATE_CATEGORY = "create table Category ("
+ "id integer primary key autoincrement, "
+ "category_name text, "
+ "category_code integer)";
private Context mContext;
public MyDataBaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
= context;
mContext }
@Override
public void onCreate(SQLiteDatabase db) {
.execSQL(CREATE_BOOK);
db.makeText(mContext, "创建数据库成功!", Toast.LENGTH_SHORT).show();
Toast}
//当数据库版本号不一样的时候调用,一般是app升级后使用
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
.execSQL("drop table if exists Book");
db.execSQL("drop table if exists Category");
dbonCreate(db);
}
}
使用:
= new MyDataBaseHelper(this,"BookStore.db",null,1);
MyDataBaseHelper dbHelper
//当磁盘没有满的时候getWritableDatabase和getReadableDatabase返回的实例都是可读可写的,没有区别;只有当磁盘满了的时候,打开数据库失败,这时getReadableDatabase会继续尝试以只读方式打开数据库,而getWritableDatabase就会直接报错
= dbHelper.getWritableDatabase();
SQLiteDatabase db
= new ContentValues();
ContentValues values
//插入数据
//如果insert的第三个参数为空,则表示插入一条除主键值以外其他字段为Null值的记录,为了满足SQL语法的需要, insert语句必须给定一个字段名,如:insert into person(name) values(NULL),倘若不给定字段名 , insert语句就成了这样: insert into person() values(),显然这不满足标准SQL的语法,所以第二个参数就是用来指定这种时候空字段的名称的,如果第三个参数values不为null,则可以把第二个参数设置为null
// 开始组装第一条数据
.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
valueslong rowid = db.insert("Book", null, values);//返回新添记录的行号,与主键id无关
.clear();
values// 开始组装第二条数据
.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
values= db.insert(“Book”,null,values);//返回新添记录的行号,与主键id无关
rowid .clear();
values
//修改数据
.put("price",100);
values.update("Book",values,"name = ?",new String[]{"The Da Vinci Code"});
db.clear();
values
//删除数据
.delete("Book","page > ?",new String[]{"500"});
db
//查询数据
//query(table, columns, selection, selectionArgs, groupBy, having, orderBy, limit)
//从Book表中查询name字段中含有"The"的记录,按price降序,并对排序的结果跳过第一条记录,最多只获取五条记录
Cursor cursor = db.query("Book", new String[]{"name","author","pages","price"}, "name like ?", new String[]{"%The%"}, null, null, "price desc","1,5");
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
String name = cursor.getString(cursor.getColumnIndex("name"));
String author = cursor.getString(cursor.getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
} while (cursor.moveToNext());
}
//关闭表指针
.close();
cursor
//关闭数据库
.close() db
事务处理:
= new MyDataBaseHelper(this,"BookStore.db",null,1);
MyDataBaseHelper dbHelper = dbHelper.getWritableDatabase();
SQLiteDatabase db
.beginTransaction();
dbtry{
.execSQL("update Book set price=18.4 where name=?",new String[]{"The Da Vinci Code"});
dbint i = 10/0;//故意设置一个异常
.execSQL("update Book set price=20.3 where name=?",new String[]{"The Lost Symbol"});
db.setTransactionSuccessful();
db}catch(Exception e){
// TODO
}finally{
.endTransaction();
db}
.close(); db
用到的权限
user-permission android:name="android.permission.INTERNET"/> <
HttpClient在Android6.0以上已被移除,但仍然可以通过Apache提供的jar包来使用,在build.gradle
中配置
android {
useLibrary 'org.apache.http.legacy'
}
Get请求:
= new DefaultHttpClient();
HttpClient httpCient = httpClient.getParams();
HttpParams params .setConnectionTimeout(params,5000);
HttpConnectionParams
= new HttpGet("http://www.baidu.com");
HttpGet httpGet try{
= httpCient.execute(httpGet);
HttpResponse httpResponse if (httpResponse.getStatusLine().getStatusCode() == 200){
= httpResponse.getEntity();
HttpEntity entity String response = EntityUtils.toString(entity,"utf-8");
}
}catch(Exception e){
// TODO
}finally{
.getConnectionManager().shutdown();
httpClient}
Post请求:
= new DefaultHttpClient();
HttpClient httpCient = httpClient.getParams();
HttpParams params .setConnectionTimeout(params,5000);
HttpConnectionParams
List<NameValuePair> list = new ArrayList<NameValuePair>();
.add(new BasicNameValuePair("key", "value"));
listString result = "";
try{
= new UrlEncodedFormEntity(list, "UTF-8");
UrlEncodedFormEntity entity = new HttpPost("http://127.0.0.1");
HttpPost httpPost .setEntity(entity);
httpPost= client.execute(httpPost);
HttpResponse httpResponse if (httpResponse.getStatusLine().getStatusCode() == 200){
InputStream inputStream = httpResponse.getEntity().getContent();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int len = 0;
if (inputStream != null) {
try {
while ((len = inputStream.read(data)) != -1) {
.write(data, 0, len);
outputStream}
= new String(outputStream.toByteArray(), "UTF-8");
result }catch(Exception e){}
}
}
}catch(Exception e){
// TODO
}finally{
.getConnectionManager().shutdown();
httpClient}
GET请求:
HttpURLConnection connection = null;
BufferedReader reader = null;
String result = "";
try{
URL url = new URL("http://127.0.0.1");
= (HttpURLConnection)url.openConnection();
connection .setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setRequestProperty("Cookie", "AppName=" + URLEncoder.encode("你好", "UTF-8"));
connection
if(connection.getResponseCode() == 200){
InputStream in = connection.getInputStream();
= new BufferedReader(new InputStreamReader(in));
reader StringBuilder response = new StringBuilder();
String line;
while((line = reader.readLine()) != null){
.append(line);
response}
= response.toString();
result }
}catch(Exception e){
//TODO
}finally{
if (reader != null){
try {
.close();
reader} catch (IOException e) {
.printStackTrace();
e}
}
if (connection != null){
.disconnect();
connection}
}
POST请求:
HttpURLConnection connection = null;
BufferedReader reader = null;
String result = "";
try{
URL url = new URL("http://127.0.0.1");
= (HttpURLConnection)url.openConnection();
connection .setRequestMethod("POST");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setDoOutput(true);
connection
FileInputStream file = new FileInputStream("filename.png");
OutputStream os = connection.getOutputStream();
int count = 0;
while((count = file.read()) != -1){
.write(count);
os}
.flush();
os.close();
os
if(connection.getResponseCode() == 200){
InputStream in = connection.getInputStream();
= new BufferedReader(new InputStreamReader(in));
reader StringBuilder response = new StringBuilder();
String line;
while((line = reader.readLine()) != null){
.append(line);
response}
= response.toString();
result }
}catch(Exception e){
//TODO
}finally{
if (reader != null){
try {
.close();
reader} catch (IOException e) {
.printStackTrace();
e}
}
if (connection != null){
.disconnect();
connection}
}
HTTPS
是HTTP over SSL/TLS
,HTTP
是应用层协议,TCP
是传输层协议,在应用层和传输层之间,增加了一个安全套接层SSL
/TLS
,SSL
/TLS
层负责客户端和服务器之间的加解密算法协商、密钥交换、通信连接的建立,对HTTP
而言,安全传输层是透明不可见的。
公钥基础设施(PKI)
假设现在A与B建立安全的连接进行通信
为了解决上述问题,引入了一个第三方,也就是CA
CA(Certificate Authority)
CA
用自己的私钥签发数字证书,数字证书中包含B的公钥。然后A可以用CA
的Root证书
(根证书)中的公钥来解密CA
签发的证书,从而拿到合法的公钥
那么又引入了一个问题,如何保证CA
的公钥是合法的呢。答案就是现代主流的浏览器会内置CA
的证书。
RA(Registration Authority)
RA
又叫中间证书,当CA
的Root证书
遭到破坏或者泄露时,由此CA
颁发的其他证书就全部失去了安全性,所以现在主流的商业数字证书机构CA
一般都是提供三级证书:Root证书
签发中级RA
证书,由RA
证书签发用户使用的证书。这样Root证书
可以离线存储来确保安全,即使RA
证书出了问题,还可以用Root证书
重新签署中间证书
使用openssl生成自签名证书
# 使用idea加密算法生成key(我们的私钥)
openssl genrsa -idea -out xxx.key 1024
# 根据key生成csr(证书签名请求,可以理解为公钥)
openssl -req -new -key xxx.key -out xxx.csr
# 把key和csr打包成crt(CA机构通过公钥和我们上传的私钥生成crt)
openssl x509 -req -days 365 -in xxx.csr -signkey xxx.key -out xxx.crt
# 其他用法
# 去掉key中的密码(加密私钥转非加密)
openssl rsa -in xxx.key -out xxx_nopass.key
# 直接生成key和crt
openssl req -days 365 -x509 -sha256 -nodes -newkey rsa:2048 -keyout xxx.key -out xxx.crt
验证过程:
说明:
在协商随机秘钥(及对应的加密算法)的过程中使用的是非对称加密,通过公钥加密的数据只能使用私钥解出来(反之亦然),协商完成后双方都知道一个随机秘钥(及对应的加密算法),以后加密的数据都是用对称加密(加密解密用同一秘钥)
之所以在可以使用非对称加密来加密传输的数据的同时,还要协商一个对称加密的秘钥(及对应的加密算法),是因为非对称加密需要复杂的计算,要消耗大量的资源,所以希望使用对称加密,但是如果直接使用静态的对称加密秘钥又无法保障秘钥安全,所以才采用通过非对称加密协商一个动态生成的对称加密秘钥(及对应的加密算法)
URL url = new URL("https://google.com");
HttpsURLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
此时使用的是默认的SSLSocketFactory
,与下段代码使用的SSLContext
是一致的
private synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
.init(null, null, null);
sslContextreturn sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
默认的SSLSocketFactory
校验服务器的证书时,会信任设备内置的100多个根证书
此方法的特点:
CA
签发的数字证书的网站才可以正常访问,私有CA
签发的数字证书的网站无法访问如果要使用私有CA
签发的证书或自签名证书,必须自定义sslContext
或直接重写校验证书链TrustManager
(或其子类X509TrustManager
)中的方法
TLS
是基于X.509
认证的
private synchronized SSLSocketFactory getMySSLSocketFactory() throws Exception {
try {
// 生成SSLContext对象
SSLContext sslContext = SSLContext.getInstance("TLS");
// 从assets中加载我们自己的证书文件
InputStream inStream = Application.getInstance().getAssets().open("anchor.cer");
CertificateFactory cerFactory = CertificateFactory.getInstance("X.509");
Certificate cer = cerFactory.generateCertificate(inStream);
// 密钥库,假设我们的私钥使用PKCS12打包,没有口令保护
KeyStore keyStore = KeyStore.getInstance("PKCS12");
.load(null, null);
keyStore.setCertificateEntry("anchor", cer);// 加载证书到密钥库中
keyStore
// 密钥管理器
KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
.init(keyStore, null);// 加载密钥库到管理器
keyFactory
// 信任管理器
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
.init(keyStore);// 加载密钥库到信任管理器
trustFactory
// 初始化
//要注意的是SecureRandom在Android4.4以前有漏洞(当用户没有提供用于产生随机数的种子时,程序不能正确调整偏移量,导致伪随机数生成器(PRNG)生成随机序列的过程可被预测),在Android4.4以前的系统最好使用第三方框架(如NoHttp,已修复该漏洞),否则要自己修复这个漏洞
.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom());
sslContext
return sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
如果要信任所有的HTTPS
连接,可以使用null
秘钥管理器和一个没有校验代码的TrustManager
(不推荐)
= SSLContext.getInstance("TLS");
sslContext .init(null, new TrustManager[]{new X509TrustManager() {
sslContext@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}}, new SecureRandom());
使用我们自定义的SSLSocketFactory
URL url = new URL("https://google.com");
HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
.setSSLSocketFactory(getMySSLSocketFactory());
urlConnectionInputStream in = urlConnection.getInputStream();
在握手期间,如果 URL
的主机名和服务器的标识主机名不匹配,则验证机制可以回调HostnameVerifier
来确定是否应该允许此连接
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
//使用
.setHostnameVerifier(hostnameVerifier); urlConnection
使用前先在build.gradle中添加依赖
compile 'com.android.volley:volley:1.1.1'
GET请求:
= Volley.newRequestQueue(context);
RequestQueue mQueue
= new StringRequest("http://www.baidu.com",
StringRequest stringRequest new Response.Listener<String>() {
public void onResponse(String response) {
// TODO
}
}, new Response.ErrorListener() {
public void onErrorResponse(VolleyError error) {
//TODO
}
});
.add(stringRequest);//执行请求 mQueue
POST请求:
= Volley.newRequestQueue(context);
RequestQueue mQueue
= new StringRequest(Method.POST, "http://127.0.0.1",
StringRequest stringRequest new Response.Listener<String>() {
public void onResponse(String response) {
// TODO
}
}, null) {
//重写getParams,设置POST的参数
@Override
protected Map<String, String> getParams() throws AuthFailureError {
Map<String, String> map = new HashMap<String, String>();
.put("params1", "value1");
map.put("params2", "value2");
mapreturn map;
}
};
.add(stringRequest);//执行请求 mQueue
= Volley.newRequestQueue(context);
RequestQueue mQueue
= new JsonObjectRequest("http://127.0.0.1", null,
JsonObjectRequest jsonObjectRequest new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
// TODO
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// TODO
}
});
.add(jsonObjectRequest); mQueue
= Volley.newRequestQueue(context);
RequestQueue mQueue
//ImageRequest(URL,responseListener,maxwidth,maxheight,ColorProperties,errorListener),其中maxwidth、maxheight设成0表示不限制图片大小
= new ImageRequest(
ImageRequest imageRequest "http://127.0.0.1/pic.png",
new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
// TODO
}
}, 0, 0, Bitmap.Config.RGB_565, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// TODO
}
});
.add(imageRequest); mQueue
初始化
;
RequestQueue mQueue;
ImageCache mImageCache;
ImageLoader mImageLoader
private void init(Context context) {
= Volley.newRequestQueue(context);
mQueue .ImageCacheParams cacheParams = new ImageCache.ImageCacheParams(context, FileManager.CACHE_IMAGE_PATH_NEW);
ImageCache.setMemCacheSizePercent(context, 0.2f);//缓存图片大小为原图的0.2倍
cacheParams= new ImageCache(cacheParams);
mImageCache = new ImageLoader(mQueue, imageCache);//注意导的是Volley下的ImageLoader
mImageLoader }
或者使用自定义ImageCache
class VolleyImageCache implements ImageLoader.ImageCache {
//LruCatch是Android提供的缓存工具
private LruCache<String, Bitmap> mCache;
public VolleyImageCache() {
int maxCacheSize = 1024 * 1024 * 10;
= new LruCache<String, Bitmap>(maxCacheSize) {
mCache //测量Bitmap的大小 ,便于统计缓存使用总量
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
}
@Override
public Bitmap getBitmap(String url) {
return mCache.get(url);
}
//缓存图片,以url作为key值
@Override
public void putBitmap(String url, Bitmap bitmap) {
.put(url, bitmap);
mCache}
}
使用
if (mImageCache.getBitmapFromMemCache(url) != null) {
.setImageBitmap(mImageCache.getBitmapFromMemCache(url));
imageView} else {
.ImageContainer container = mImageLoader.get(url, TransitionImageListener.obtain(imageView, R.drawable.loading_pic, R.drawable.default_pic), width, height);
ImageLoader.setTag(container);
imageView}
在xml中使用volley的NetworkImageView
com.android.volley.toolbox.NetworkImageView
< android:id="@+id/network_image_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal" />
JAVA代码
= (NetworkImageView) findViewById(R.id.nivTestView);
NetworkImageView network_image_view .setDefaultImageResId(R.drawable.default_pic);
network_image_view.setErrorImageResId(R.drawable.error_pic);
network_image_view.setImageUrl(url, mImageLoader); network_image_view
原理
Range
头字段指定每条线程从文件的什么位置开始下载,下载到什么位置为止,Range
头需要服务器支持RandomAccessFile
可以从文件指定位置开始写入所以我们先计算每条线程要下载的Range
,然后再通过RandomAccessFile
的seek
函数指定写入位置即可
实现
class DownloadUtils {
public static ArrayList<Thread> download(String url, String fileDir, int threadCount, DownloadThread.Callback callback) throws IOException {
Long size;
ArrayList<Thread> threadList = new ArrayList<>(threadCount);
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
.setConnectTimeout(15 * 1000);
connection.setRequestMethod("GET");
connection.connect();
connectionif (connection.getResponseCode() == 200) {
= (long) connection.getContentLength();
size } else {
throw new IllegalStateException("连接失败");
}
RandomAccessFile raf = new RandomAccessFile(new File(fileDir), "rw");
.setLength(size);
raf.close();
raf
long block = size % threadCount == 0 ? size / threadCount : size / threadCount + 1;
for (int i = 0; i < threadCount; i++) {
long start = i * block;
long end = start + block >= size ? size : start + block - 1;
Thread thread = new DownloadThread(url, fileDir, start, end, callback);
.add(thread);
threadList.start();
thread}
return threadList;
}
}
class DownloadThread extends Thread {
String url;
String fileDir;
long start;
long end;
long progress;
Callback callback;
DownloadThread(String url, String fileDir, long start, long end, Callback callback) {
this.url = url;
this.fileDir = fileDir;
this.start = start;
this.end = end;
this.callback = callback;
}
@Override
public void run() {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
.setConnectTimeout(15 * 1000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg," +
conn" image/pjpeg, application/x-shockwave-flash, application/xaml+xml, " +
"application/vnd.ms-xpsdocument, application/x-ms-xbap, " +
"application/x-ms-application, application/vnd.ms-excel, " +
"application/vnd.ms-powerpoint, application/msword, */*");
.setRequestProperty("Referer", url);
conn.setRequestProperty("Range", "bytes=" + start + "-" + end);// 设置获取实体数据的范围
conn.setRequestProperty("Connection", "Keep-Alive");
conn.connect();
conn
//如果服务器支持Range头,则返回的状态码是206
if (conn.getResponseCode() == 200 || conn.getResponseCode() == 206) {
InputStream is = conn.getInputStream();
int len;
byte[] buf = new byte[1024];
RandomAccessFile raf = new RandomAccessFile(new File(fileDir), "rwd");
.seek(start);
raf
while ((len = is.read(buf)) != -1) {
.write(buf, 0, len);
raf+= len;
progress .onProgress(progress);
callback}
.close();
raf.close();
is.onFinish();
callback} else {
.onError(new Exception("网络错误,请求失败,状态码为" + conn.getResponseCode()));
callback}
} catch (Exception e) {
.onError(e);
callback}
}
interface Callback {
void onProgress(long progress);
void onFinish();
void onError(Exception e);
}
}
public class MyHandlerActivity extends Activity{
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg){
switch(msg.what){
case 1:
.makeText(this,(String)msg.obj,0).show();
Toastbreak;
case 2: break;
default: break;
}
}
};
void onButtonClick(view v){
new Thread(new Runnable{
public void run(){
try{
Thread.sleep(2000);
}catch(Exception e){
// TODO
}
}
= Message.obtain();
Message msg .what = 1;
msg.obj = "Hello";
msg.sendMessage(msg);
handler}).start();
}
}
除此以外,还有
.post(Runnable);
handler.postAtTime(Runnable, long);
handler.postDelayed(Runnable, long);
handler.sendEmptyMessage(int);//直接指定msg.what,其实内部会构造一个msg
handler.sendMessage(Message);
handler.sendMessageAtTime(Message, long);
handler.sendMessageDelayed(Message, long);//延迟指定时间发消息 handler
使用Handler
的sendMessage
等方法进行线程间通信实际上是把message放到了一个叫MessageQueue
的消息队列中,然后由一个叫Looper
的类不断的对MessageQueue
进行遍历(epoll
方式的轮询,epoll
是linux提供的一种多路复用的机制,可以参考JAVA中NIO的实现原理),取出并执行message
UI线程默认会在启动时创建Looper
,所以UI线程可以直接使用Handler
,而如果UI线程需要往子线程中发消息,需要我们手动为子线程创建Looper
,并执行遍历操作。Looper
的构造函数是private
的,我们只能通过静态方法创建
class MyThread extends Thread {
public Handler mHandler;
public void run() {
//创建Looper
.prepare();
Looper
= new Handler() {
mHandler public void handleMessage(Message msg) {
// process incoming messages here
}
};
//执行轮询,在执行完任务后需要使用Looper.myLooper().quit()来停止轮询,否则可能会导致内存泄露
.loop();
Looper}
}
Looper
内部通过ThreadLocal
的方式保存对当前线程的引用,所以Looper
是与线程绑定的,而且一个线程只能有一个Looper
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
.set(new Looper(quitAllowed));
sThreadLocal}
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//创建MessageQueue
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
.getInt("log.looper."
SystemProperties+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);
boolean slowDeliveryDetected = false;
//轮询
for (;;) {
//如果此时消息队列中有Message,那么next方法会立即返回该Message,如果此时消息队列中没有Message,那么next方法就会阻塞式地等待获取Message
= queue.next(); // might block
Message msg if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
.println(">>>>> Dispatching to " + msg.target + " " +
logging.callback + ": " + msg.what);
msg}
final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
= thresholdOverride;
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs }
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
.traceBegin(traceTag, msg.target.getTraceName(msg));
Trace}
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
try {
//msg.target是一个Handler
.target.dispatchMessage(msg);
msg= needEndTime ? SystemClock.uptimeMillis() : 0;
dispatchEnd } finally {
if (traceTag != 0) {
.traceEnd(traceTag);
Trace}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
.w(TAG, "Drained");
Slog= false;
slowDeliveryDetected }
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
)) {
msg// Once we write a slow delivery log, suppress until the queue drains.
= true;
slowDeliveryDetected }
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}
if (logging != null) {
.println("<<<<< Finished to " + msg.target + " " + msg.callback);
logging}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
.wtf(TAG, "Thread identity changed from 0x"
Log+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
.recycleUnchecked();
msg}
}
我们可以通过Looper.myLooper()
来获取当前线程绑定的Looper
,也可以通过Looper.getMainLooper()
获取UI线程的Looper
子线程是不能直接更新UI的,要在子线程更新UI,有以下方式:
Handler
,从子线程向UI线程发messagerunOnUiThread(Runnable)
方法Handler
时,指定绑定的Looper
为UI线程的Looperpost(Runnable)
方法new Thread(new Runnable() {
@Override
public void run() {
.e("TAG", "在子线程中创建handler,通过并指定绑定的Looper为MainLooper:"+Thread.currentThread().getName());
Log
Handler handler=new Handler(getMainLooper());
.post(new Runnable() {
handler@Override
public void run() {
.e("TAG", "使用子线程中创建handler往主线程发消息:"+Thread.currentThread().getName());
Log}
});
}
}).start();
对于Toast、showDialog,其内部也是使用Handler
向UI线程发起更新UI的message的,在子线程中使用Toast、showDialog我们只需要为子线程创建Looper
然后直接使用就可以了,因为其内部已经为我们完成在主线程运行的操作了
new Thread(new Runnable() {
@Override
public void run() {
.prepare();
Looper.makeText(MainActivity.this, "run on thread"+Thread.currentThread().getName(), Toast.LENGTH_SHORT).show();
Toast.loop();
Looper}
}).start();
最后,在执行完任务后需要使用Looper.myLooper().quit()来停止轮询,否则可能会导致内存泄露
这里主要讨论Handler
的内存泄露问题
延时的Handler
容易造成内存泄漏(Handler
持有activity的引用,导致activity无法回收),解决方法有两种:
MessageQueue
public void onDestory(){
.removeCallbacksAndMessages(null);
handler}
在Java中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用,静态的内部类不会持有外部类的引用,所以把handler定义成静态内部类的话,不会持有Activity的引用,Activity可以随便回收,这时还要在handler中把对Activity的引用改成弱引用(GC在回收内存的时候会忽略调弱引用,只要对象没有被强引用指向(实际上多数时候还要求没有软引用),都能被GC回收)
public static MyHandler extends Handler{
WeakReference<Activity> mWeakReference;
public MyHandler(Activity activity){
=new WeakReference<Activity>(activity);
mWeakReference}
@Override
public void handleMessage(Message msg){
final Activity activity=mWeakReference.get();
if(activity!=null){
if (msg.what == 1){
// TODO
}
}
}
}
= Volley.newRequestQueue(context);
RequestQueue mQueue new AsyncTask<String, Integer, Bitmap>(){
//在doInBackground之前执行
protected void onPreExecute() {
//在主线程执行
}
protected Bitmap doInBackground(String... args1) {
//在子线程执行
= null;
Bitmap bitmap = new ImageRequest(args1[0],
ImageRequest imageRequest new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
= response;
bitmap }
}, 0, 0, Bitmap.Config.RGB_565, null);
.add(imageRequest);
mQueuefor (int i = 0; i < times; i++) {
publishProgress(i);//提交之后,会执行onProcessUpdate方法
}
return bitmap;
}
//在doInbackground之后执行
protected void onPostExecute(Bitmap args3) {
//在主线程执行
}
//在调用cancel方法后会执行到这里
protected void onCancelled() {
// TODO
}
protected void onProgressUpdate(Integer... args2) {
//在主线程执行,一般用于更新进度条
}
}.execute("http://127.0.0.1/pic.png");
JSONObject.toBean的数据是用{ }
来表示的,如:{ "id" : "123", "name" : "小明"}
JSONArray是JSONObject的数组,用[ ]
表示,如[ { "id" : "123", "name" : "小明"}, { "id" : "124", "name" : "小红"} ]
//创建,根据json字符串的不同选择创建不同的对象
= new JSONObject(String str);
JSONObject jsonObject = new JSONArray(String str);
JSONArray jsonArray
//JSONArray通过index获取JSONObject
= (JSONObject)jsonArray.get(0);
JSONObject jsonObject //获取JSONObject内容
int id = jsonObject.getInt("id");
String name = jsonObject.getString("name");
//List转JSON,Map、Object同理,但要求Object是JavaBean(有get、set方法)
ArrayList<String> list = new ArrayList<String>();
.add("java");
list.add("android");
list= JSONArray.fromObject(list); JSONArray jsonarray
使用前先在build.gradle中添加依赖
implementation 'com.google.code.gson:gson:2.8.5'
使用
= new Person(11,"小明",“Man”);//Person是一个JavaBean
Person person String jsonStr = new Gson.toJson(person, Person.class);//Object转json
= new Gson.fromJson(jsonStr, Person.class);//json转Object
Person person2
/*
json转List,json转Map同理,第二个参数是TypeToken的子类
getType()作用是获取父类泛型:
ParameterizedType pt = (ParameterizedType)getClass().getGenericSuperclass();
Class clazz = (Class)pt.getActualTypeArguments()[0];
*/
List<Person> list = new Gson.fromJson(jsonStr, new TypeToken<List<Person>>(){}.getType());
//Gson的注解,serialize和deserialize默认为true,serialize为true表示toJson时会序列化该属性,deserialize为true表示fromJson生成Java对象时会反序列化;SerializedName指定该字段在序列化成json时的名称
class Person{
@Expose
private int age;
@Expose(serialize = true)
private String name;
@Expose(deserialize = false)
private String sex;
@SerializedName("addr")
private String address;
//以下省略get、set方法
}
//默认情况下@Expose注解是不起作用的,需要用GsonBuilder创建Gson的时候调用了GsonBuilder.excludeFieldsWithoutExposeAnnotation()方法
//使用GsonBuilder创建Gson可以更改Gson的默认参数
= new GsonBuilder().setVersion(1.0)
Gson gson2 .excludeFieldsWithoutExposeAnnotation()
.create();
onFinishInflate()->onAttachToWindow()->measure()->onMeasure()->onSizeChanged()->layout()->onLayout()->draw()->onDraw()->onDetachedFromWindow
,自定义View一般要重写onMeasure和onDraw,自定义ViewGroup要重写onMeasure和onLayout;onMeasure和onLayout一般要调用多次才能完成测量和布局
onMeasure决定当前控件的宽高,其参数可以通过MeasureSpec.getMode()、MeasureSpec.getSize()解析;ViewGroup重写该方法主要是逐个测量子view的宽高(for(int i=0;i<getChildCount();i++){getChildAt(i).onMeasure(widthMeasureSpec,heightMeasureSpec);}
),如果不重写该方法,而直接在onLayout指定大小的话,如果子view是ViewGroup,则无法显示该ViewGroup中的内容
onLayout指定子布局的位置或排列方式(水平、垂直),可以通过requestLayout强制重新布局
onDraw可以设置view的样式,常用的类有ShapeDrawable、Canvas、Paint,可以通过invalid或postInvalid强制重新绘制
默认情况下,事件从Activity开始,按照dispatchTouchEvent()
->onInterceptTouchEvent()
的顺序,从最顶层的控件传到点击处的控件,然后调用被点击控件的OnTouchListener
->onTouchEvent()
->OnLongClickListener
->OnClickListener
;某些情况下,事件还可以从子控件回传到父控件中
dispatchTouchEvent()
:
onTouchEvent()
方法进行消费;return super.dispatchTouchEvent(e)
,则表示按照默认的方式处理,事件会继续往后分发,此时当前view的onIntercepterTouchEvent()
方法会捕获该事件,判断需不需要进行事件的拦截;onInterceptTouchEvent()
:
dispatchTouchEvent()
方法进行分发处理;onTouchEvent()
方法进行处理;return super.onInterceptTouchEvent(e)
,则表示按照默认的方式处理,如果当前控件就是被点击的控件,则拦截事件,并交由当前view的onTouchEvent()
方法处理,如果点击的是子控件,则不拦截事件,继续把事件分发到子控件;OnTouchListener
:
onTouch()
返回false,则事件继续往后传递,交由onTouchEvent()
处理;OnTouchListener
优先级比onTouchEvent()
高,所以onTouchEvent()
不会被调用,事件到此结束;onTouchEvent()
:
onTouchEvent()
事件处理,如果最上层的view或Activity的onTouchEvent()
还是返回false,则该事件将消失,接下来的一系列事件都将会直接被上层的onTouchEvent()
方法捕获;return super.onTouchEvent(e)
,则表示按照默认的方式处理,效果和返回false一样;OnLongClickListener
和OnClickListener
,只有onTouchEvent()
不返回true才会被触发(实测如果调用了super.onTouchEvent(e)
但是返回true也可以触发)父控件要拦截事件,则可以重写onInterceptTouchEvent()
,并返回true,这样子控件就不会得到该事件,但子控件可以通过getParent().requestDisallowInterceptTouchEvent()
来禁止拦截,该函数优先级比onInterceptTouchEvent()
高
我们给LinearLayout重写dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
,并给它设置OnTouchListener
、OnClickListener
、OnLongClickListener
初始状态,没有任何拦截、消费的情况下:
@Override
public boolean onTouchEvent(MotionEvent event) {
System.out.println("onTouchEvent action=" + event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
System.out.println("dispatchTouchEvent action=" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
System.out.println("onInterceptTouchEvent action=" + event.getAction());
return super.onInterceptTouchEvent(event);
}
.setOnTouchListener((view, motionEvent) -> {
linearLayoutSystem.out.println("OnTouchListener action=" + motionEvent.getAction());
return false;
});
.setOnClickListener((view) -> System.out.println("OnClickListener"));
linearLayout
.setOnLongClickListener(view -> {
linearLayoutSystem.out.println("OnLongClickListener");
return false;
});
打印(ACTION_DOWN = 0
、ACTION_UP = 1
、ACTION_MOVE = 2
)
dispatchTouchEvent action=0
onInterceptTouchEvent action=0
onTouchListener action=0
onTouchEvent action=0
dispatchTouchEvent action=2
onTouchListener action=2
onTouchEvent action=2
onLongClickListener
dispatchTouchEvent action=1
onTouchListener action=1
onTouchEvent action=1
onClickListener
把linearLayout的super.onTouchEvent
删掉,直接返回true
(表示消费了onTouchEvent
事件)
@Override
public boolean onTouchEvent(MotionEvent event) {
System.out.println("onTouchEvent action=" + event.getAction());
return true;
}
这时OnLongClickListener
和OnClickListener
都不会触发
dispatchTouchEvent action=0
onInterceptTouchEvent action=0
onTouchListener action=0
onTouchEvent action=0
dispatchTouchEvent action=2
onTouchListener action=2
onTouchEvent action=2
dispatchTouchEvent action=1
onTouchListener action=1
onTouchEvent action=1
如果保留super.onTouchEvent
,但返回true
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
System.out.println("onTouchEvent action=" + event.getAction());
return true;
}
这时OnLongClickListener
和OnClickListener
可以正常触发,说明点击和长按事件是由super.onTouchEvent
调用的,只要调用了super.onTouchEvent
,无论我们有没有消费事件点击和长按都能正常执行
dispatchTouchEvent action=0
onInterceptTouchEvent action=0
onTouchListener action=0
onTouchEvent action=0
onLongClickListener
dispatchTouchEvent action=2
onTouchListener action=2
onTouchEvent action=2
dispatchTouchEvent action=1
onTouchListener action=1
onTouchEvent action=1
onClickListener
把代码恢复到初始状态,这时,如果我们在setOnLongClickListener
返回true
.setOnLongClickListener(view -> {
linearLayoutSystem.out.println("OnLongClickListener");
return true;
});
这时OnClickListener
不会被触发
dispatchTouchEvent action=0
onInterceptTouchEvent action=0
onTouchListener action=0
onTouchEvent action=0
onLongClickListener
dispatchTouchEvent action=2
onTouchListener action=2
onTouchEvent action=2
dispatchTouchEvent action=1
onTouchListener action=1
onTouchEvent action=1
把代码恢复到初始状态,这时,如果我们在linearLayout
的onInterceptTouchEvent
中返回true
,给linearLayout
添加一个子控件button
,并给重写button的dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
,设置OnTouchListener
、OnClickListener
、OnLongClickListener
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
System.out.println("linearLayout: onInterceptTouchEvent action=" + event.getAction());
return true;
}
对自己没有任何影响,但子控件会接收不到任何事件
linearLayout: dispatchTouchEvent action=0
linearLayout: onInterceptTouchEvent action=0
linearLayout: onTouchListener action=0
linearLayout: onTouchEvent action=0
linearLayout: onLongClickListener
linearLayout: dispatchTouchEvent action=2
linearLayout: onTouchListener action=2
linearLayout: onTouchEvent action=2
linearLayout: dispatchTouchEvent action=1
linearLayout: onTouchListener action=1
linearLayout: onTouchEvent action=1
linearLayout: onClickListener
而在没有拦截的时候是
linearLayout: dispatchTouchEvent action=0
linearLayout: onInterceptTouchEvent action=0
button: dispatchTouchEvent action=0
button: onTouchListener
button: onTouchEvent action=0
button: onLongClickListener
linearLayout: dispatchTouchEvent action=2
linearLayout: onInterceptTouchEvent action=2
button: dispatchTouchEvent action=2
button: onTouchListener
button: onTouchEvent action=2
linearLayout: dispatchTouchEvent action=2
linearLayout: onInterceptTouchEvent action=2
button: dispatchTouchEvent action=2
button: onTouchListener
button: onTouchEvent action=2
linearLayout: dispatchTouchEvent action=1
linearLayout: onInterceptTouchEvent action=1
button: dispatchTouchEvent action=1
button: onTouchListener
button: onTouchEvent action=1
button: onClickListener
总结
在没有消费事件的情况下,事件的传递流程是
st=>start: 用户触摸
f1=>operation: ViewGroup控件dispatchTouchEvent
f2=>operation: ViewGroup控件onInterceptTouchEvent
f3=>operation: View控件dispatchTouchEvent
f4=>operation: View控件OnTouchListener
f5=>operation: View控件onTouchEvent
cond1=>condition: 继续触摸
f6=>operation: View控件OnLongClickListener
f7=>operation: View控件OnClickListener
st->f1->f2->f3->f4->f5->cond1
cond1(no)->f6->f7
cond1(yes)->f1
如果父View
在OnInterceptTouchEvent
中返回true,则子View
所有事件都不会接收到(子类可以通过getParent().requestDisallInterceptRouchEvent(true)
来禁止父View
拦截),此时子View
的onTouchEvent
事件交由父View
的onTouchEvent
处理
如果onInterceptTouchEvent
在ACTION_DOWN
的时候返回false
,后面的ACTION_MOVE
和ACTION_UP
都不会再调用onInterceptTouchEvent
(如果在ACTION_DOWN
的时候返回true
,后面的事件还能收到)
上面任何一个函数返回true
,代表消费了事件,则后面的函数都不会继续执行,但OnLongClickListener
和OnClickListener
是通过super.onTouchEvent
实现的,只要调用了super.onTouchEvent
,无论有没有消费事件,都可以触发
上面的图画得不够准确,OnLongClickListener
会在触摸一段事件后触发,触发后如果用户还在长按,还会继续执行ViewGroup控件dispatchTouchEvent
到View控件onTouchEvent
,但只会触发OnLongClickListener
一次,而OnClickListener
总是在最后触发的(ACTION_UP
)
参考github项目SwipeBackLayout
原理
Activity本身是不可以滑动的,我们滑动的其实是 Activity里面的可见元素,而我们将Activity设置为透明的,滑动时,由于Activity的底部是透明的,我们就可以在滑动过程中看到下面的Activity,这样看起来就是在滑动 Activity
实现
设置Activity透明背景、切换动画
style name="AppTheme.TransparentActivity" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@style/SlideRightAnimation</item>
<style> </
设置Activity进入和退出的动画
style name="SlideRightAnimation" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/slide_in_right</item>
<item name="android:activityOpenExitAnimation">@null</item>
<item name="android:activityCloseEnterAnimation">@null</item>
<item name="android:activityCloseExitAnimation">@anim/slide_out_right</item>
<item name="android:taskOpenEnterAnimation">@anim/slide_in_right</item>
<item name="android:taskOpenExitAnimation">@null</item>
<item name="android:taskCloseEnterAnimation">@null</item>
<item name="android:taskCloseExitAnimation">@anim/slide_out_right</item>
<item name="android:taskToFrontEnterAnimation">@anim/slide_in_right</item>
<item name="android:taskToFrontExitAnimation">@null</item>
<item name="android:taskToBackEnterAnimation">@null</item>
<item name="android:taskToBackExitAnimation">@anim/slide_out_right</item>
<item name="android:windowEnterAnimation">@anim/slide_in_right</item>
<item name="android:windowExitAnimation">@anim/slide_out_right</item>
<style> </
slide_in_right.xml
<?xml version="1.0" encoding="utf-8"?>
translate xmlns:android="http://schemas.android.com/apk/res/android"
< android:duration="250"
android:fromXDelta="100%p"
android:toXDelta="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
translate> </
slide_out_right.xml
<?xml version="1.0" encoding="utf-8"?>
translate xmlns:android="http://schemas.android.com/apk/res/android"
< android:duration="250"
android:fromXDelta="0"
android:toXDelta="100%p"
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
translate> </
我们的view都放在DecorView
中(可以通过HierarchyView
工具查看,该工具在sdk的tools目录下),我们把自己的SwipeBackLayout,加到DecorView
中,然后通过监听SwipeBackLayout的onTouchEvent
,可以判断是否需要滑动返回
把自己(SwipeBackLayout)替换为DecorView
的第一个子view
// 在DecorView下增加SwipeBackLayout(FragmentLayout)
= (ViewGroup) activity.getWindow().getDecorView();
ViewGroup decor // 拿到第一个子view-decorChild
= (ViewGroup) decor.getChildAt(0);
ViewGroup decorChild .LayoutParams params = decorChild.getLayoutParams();
ViewGroup// 删除子view-decorChild
.removeView(decorChild);
decor// 把子view-decorChild添加到SwipeBackLayout(FragmentLayout)下
this.addView(decorChild);
// 把SwipeBackLayout(FragmentLayout)添加到DecorView下
.addView(this, params); decor
监听事件
;
ViewGroup decorChildboolean swipeEnabled = true;
boolean canSwipe = false;
boolean ignoreSwipe = false;
float downX;
float downY;
float lastX;
float currentX;
float currentY;
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
if (swipeEnabled && !canSwipe && !ignoreSwipe) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
= ev.getX();
downX = ev.getY();
downY = downX;
currentX = downY;
currentY = downX;
lastX break;
case MotionEvent.ACTION_MOVE:
float dx = ev.getX() - downX;
float dy = ev.getY() - downY;
if (dx * dx + dy * dy > touchSlop * touchSlop) {
if (dy == 0f || Math.abs(dx / dy) > 1) {
= ev.getX();
downX = ev.getY();
downY = downX;
currentX = downY;
currentY = downX;
lastX = true;
canSwipe return true;
} else {
= true;
ignoreSwipe }
}
break;
}
}
if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
= false;
ignoreSwipe }
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canSwipe || super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (canSwipe) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
= event.getX();
downX = event.getY();
downY = downX;
currentX = downY;
currentY = downX;
lastX break;
case MotionEvent.ACTION_MOVE:
= event.getX();
currentX = event.getY();
currentY float dx = currentX - lastX;
if (getContentX() + dx < 0) {
.setX(0);
decorChild} else {
.setX(decorChild.getX() + dx);
decorChild}
= currentX;
lastX invalidate();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
= false;
canSwipe //回弹
//...
break;
default:
break;
}
}
return super.onTouchEvent(event);
}
shape可以自定义图形,对应JAVA代码中的ShapeDrawable类
<?xml version="1.0" encoding="utf-8"?>
<!-- 矩形:rectangle、椭圆:oval、线:line、圆环:ring -->
<!--
对于ring(圆环),还可以设置
android:innerRadius: 指圆环的内半径
android:thickness: 指圆环的厚度
android:innerRadiusRatio: 内半径占整个Drawable宽度的比例,默认是9,如果为n,那么内半径 = 宽度/n
android:thicknessRatio: 厚度占整个Drawable宽度的比例,默认是3,如果为n,那么厚度 = 宽度/n
android:useLevel: 官方文档建议使用false,否则可能无法达到预期显示效果。除非当做LevelListDrawable来使用
-->
shape
< xmlns:android="http://schemas.android.com/apk/res/android"
android:shape=["rectangle" | "oval" | "line" | "ring"] >
<!-- corners:圆角大小 -->
<corners
android:radius="integer"
android:topLeftRadius="integer"
android:topRightRadius="integer"
android:bottomLeftRadius="integer"
android:bottomRightRadius="integer" />
<!-- gradient:渐变 -->
gradient
< android:angle="integer"
android:centerX="integer"
android:centerY="integer"
android:centerColor="integer"
android:endColor="color"
android:gradientRadius="integer"
android:startColor="color"
android:type=["linear" | "radial" | "sweep"]
android:useLevel=["true" | "false"] />
<padding
android:left="integer"
android:top="integer"
android:right="integer"
android:bottom="integer" />
size
< android:width="integer"
android:height="integer" />
<!-- solid:填充颜色 -->
solid
< android:color="color" />
<!-- stroke:边框线 -->
stroke
< android:width="integer"
android:color="color"
android:dashWidth="integer"
android:dashGap="integer" />
</shape>
JAVA代码中还可以通过指定Path实现其他效果
//画菱形
= new Path();
Path path .moveTo(50, 0);//单位都是px,实际使用需转成dp
path.lineTo(0, 50);
path.lineTo(50, 100);
path.lineTo(100, 50);
path.close();
path
= new ShapeDrawable(new PathShape(path, 100, 100)); ShapeDrawable mDrawables
RectShape
= new RectShape();
RectShape rectShape = new ShapeDrawable(rectShape);
ShapeDrawable drawable .getPaint().setColor(Color.BLUE);
drawable.getPaint().setStyle(Paint.Style.FILL); drawable
其子类有
ArcShape: 弧形
= new ArcShape(0, 180); ArcShape arcShape
OvalShape: 椭圆
= new OvalShape(); OvalShape ovalShape
RoundRectShape: 圆角矩形
//指定外部矩形的弧度
float[] outerRadii = new float[] { 10, 10, 10, 10, 10, 10, 10, 10 };
//执行内部矩形与外部矩形的距离
= new RectF(50, 50, 50, 50);
RectF inset //内部矩形的弧度
float[] innerRadii = new float[] { 20, 20, 20, 20, 20, 20, 20, 20 };
= new RoundRectShape(outerRadii, inset, innerRadii); RoundRectShape rr
在res/values
文件下定义一个attrs.xml
文件(文件名固定为attrs.xml
,不可更改),内容如下
<?xml version="1.0" encoding="utf-8"?>
resources>
<declare-styleable name="MyAttr">
<attr name="name" format="string"/>
<attr name="myWidth" format="dimension" />
<attr name="id" format="integer"/>
<attr name="portrait" format="reference|color"/>
<declare-styleable>
</resources> </
format可以指定的属性有:
attr name="style">
<enum name="STROKE" value="0"></enum>
<enum name="FILL" value="1"></enum>
<attr> </
attr name="weight">
<flag name="fat" value="0" />
<flag name="mid" value="1" />
<flag name="thin" value="2" />
<attr> </
使用自定义属性
<?xml version="1.0" encoding="utf-8"?>
RelativeLayout
<xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:myapp="http://schemas.android.com/apk/res/com.myapp"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
com.myapp.MyView
< android:id="@+id/myview"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
myapp:name="小明"
myapp:id="5"
myapp:myWidth="50dp"
myapp:portrait="@drawable/pic_portrait"/>
RelativeLayout> </
在JAVA代码中获取属性值
= context.obtainStyledAttributes(attrs,R.styleable.MyAttr);//attrs就是当前自定义view的构造函数中的AttributeSet参数
TypedArray typedArray = typedArray.getString(R.styleable.MyAttr_name, "");//第二个参数为不匹配时的默认值
name = typedArray.getInt(R.styleable.MyAttr_id, -1);
id = typedArray.getResourceId(R.styleable.MyAttr_portrait, -1);
itemBg
.recycle();//用完记得回收 typedArray
配置
service
< android:description="string resource"
android:directBootAware=["true" | "false"]
android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:icon="drawable resource"
android:isolatedProcess=["true" | "false"]
android:label="string resource"
android:name="string"
android:permission="string"
android:process="string" >
intent-filter>
<action android:name="com.myapp.MyService" />
<intent-filter>
</service> </
:remote
的意思是在前面附上包名,相当于package_name:remote)startService方式启动:onCreate()->onStartCommand()->onDestory()
,重复调用startService则会重复调用onStartCommand,而不会新建一个Service,由于onBind方法是个抽象方法,所以即使通过startService方式启动没有用到该方法,但还是必须重写,此时将返回值设成null即可,停止服务可以用stopService()或者StopSelf();该方法一般用于一些后台操作(如播放音乐)
int onStartCommand(Intent intent, int flags, int startId)
,flag可选值有
onStartCommand的返回值有 - START_NOT_STICKY:表示当Service运行的进程被Android系统强制杀掉之后,不会重新创建该Service,如果想重新实例化该Service,就必须重新调用startService来启动 - START_STICKY:表示Service运行的进程被Android系统强制杀掉之后,Android系统会将该Service依然设置为started状态(即运行状态),但是不再保存onStartCommand方法传入的intent对象,然后Android系统会尝试再次重新创建该Service,并执行onStartCommand回调方法,这时onStartCommand回调方法的Intent参数为null,也就是onStartCommand方法虽然会执行但是获取不到intent信息 - START_REDELIVER_INTENT:表示Service运行的进程被Android系统强制杀掉之后,与返回START_STICKY的情况类似,Android系统会将再次重新创建该Service,并执行onStartCommand回调方法,但是不同的是,Android系统会再次将Service在被杀掉之前最后一次传入onStartCommand方法中的Intent再次保留下来并再次传入到重新创建后的Service的onStartCommand方法中,这样我们就能读取到intent参数 - START_STICKY_COMPATIBILITY:START_STICKY的兼容版本,但不保证服务被杀死后一定能重启
IntentService是对Service封装,其实就是一个带有工作线程的Service,重写onHandleIntent,该函数是在子线程执行的,可以在其中执行耗时任务,重复调用startService时,就会重复调用该方法,但任务只能一个一个地执行,不能同时执行多个任务,当所有任务执行完成后,就会自动调用onDestory,无需手动停止Service
bindService方式启动:onCreate()->onBind()->onUnbind()->onDestory()
,重复调用bindService则会重复调用onBind,而不会新建一个Service,在Activity销毁时必须unbindService,否则会出现内存泄漏;该方法启动的服务一般用于两个线程的通讯(如AIDL)
无论以哪种方式启动,Service都运行在它的宿主线程中,如果在Service中执行耗时操作时,主线程会卡死,会出现ANR(Android Not Response),所以一般都是在Service中创建一个新的线程来处理一些耗时工作
绑定或启动服务的时机: - 如果只需要在Activity可见时与服务交互,则应在onStart()期间绑定,在onStop()期间取消绑定 - 如果希望Activity在后台停止运行状态下仍可接收响应,则可在onCreate()期间绑定,在onDestroy()期间取消绑定 - 注意:切勿在 Activity 的 onResume() 和 onPause() 期间绑定和取消绑定,因为每一次生命周期转换都会发生这些回调,这样反复绑定与解绑是不合理的
要在客户端(Activity)使用Service的公共方法,有以下思路:
自定义IBinder,用于Service(服务端)与客户端相同的进程中运行时的通讯
public class MyService extends Service{
private MyBinder binder = new MyBinder();
//自定义Binder,有一个返回服务器实例的方法
public class MyBinder extends Binder {
getService() {
MyService return MyService.this;
}
}
//在onBind返回自定义的Binder,就可以通过该Binder获取Service实例
public IBinder onBind(Intent intent) {
return binder;
}
//公共方法
public String getName(){
this.getClass().getName();
retrun }
}
public class MainActivity extends Activity{
;
ServiceConnection connprivate MyService myService;
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
= new Intent(this, MyService.class);
Intent intent = new ServiceConnection() {
conn //绑定成功的回调
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
.MyBinder binder = (MyService.MyBinder) service;
MyService= binder.getService();
myService }
//当取消绑定的时候被回调。但正常情况下是不被调用的,它的调用时机是当Service服务被意外销毁时,例如内存的资源不足时这个方法才被自动调用
@Override
public void onServiceDisconnected(ComponentName name) {
=null;
myService}
};
bindService(intent, conn, Service.BIND_AUTO_CREATE);
}
protected void onDestory(){
if(myService != null){
= null;
myService unbindService(conn);
}
}
}
使用Messenger,可以执行进程间通信(IPC),Messenger是以串行的方式处理客户端发来的消息,这样我们就不必对服务进行线程安全设计了;由于Message和Messenger都实现了Parcelable接口,可以进程传递数据,但message.obj没有实现该接口,所以无法跨进程传输对象
客户端向服务器端发消息: ①服务器端实现一个Handler,由其接收来自客户端的每个调用的回调 ②用Handler创建Messenger对象 ③Messenger创建一个IBinder,服务通过onBind()使其返回客户端 ④客户端使用IBinder将Messenger(引用服务的Handler)实例化,然后使用Messenger将Message对象发送给服务 ⑤服务在其Handler中(在handleMessage()方法中)接收每个Message
服务器端向客户端回复消息: ①客户端实现一个Handler,用于接收服务器端的回复 ②通过Handler创建Messenger ③通过message.replyTo将Messenger绑定到message中 ④服务器端通过message.replyTo得到Messenger ⑤通过Messenger向客户端发回复
服务端:
public class MessengerService extends Service {
final Messenger mMessenger = new Messenger(new IncomingHandler());
final static int MSG_1 = 1;
final static int MSG_2 = 2;
class IncomingHandler extends Handler {
public void handleMessage(Message msg) {
switch(msg.what){
case MSG_1:
//接收到来自客户端的信息
.i("TAG", "接收到客户端的message");
Log//回复客户端
=msg.replyTo;
Messenger client=Message.obtain(null,MessengerService.MSG_2);
Message replyMsg=new Bundle();
Bundle bundle.putString("reply","ok");
bundle.setData(bundle);
replyMsgtry{
.send(replyMsg);
client}catch(RemoteException e){
.printStackTrace();
e}
break;
default: break;
}
}
}
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
}
客户端:
public class ActivityMessenger extends Activity {
= null;
Messenger mService private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
//通过服务端传递的IBinder对象,创建相应的Messenger,通过该Messenger对象与服务端进行交互
= new Messenger(service);
mService }
public void onServiceDisconnected(ComponentName className) {
= null;
mService }
};
//用于接收服务器端的回复
private Messenger mRecevierReplyMsg= new Messenger(new ReceiverReplyMsgHandler());
private static class ReceiverReplyMsgHandler extends Handler(){
public void handleMessage(Message msg){
switch (msg.what){
case MessengerService.MSG_2:
//得到服务器回复
.i("TAG",msg.getData().getString("reply"));
Logbreak;
default: break;
}
}
}
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindService(new Intent(ActivityMessenger.this, MessengerService.class), mConnection, Context.BIND_AUTO_CREATE);
if(mService!=null){
= Message.obtain(null, MessengerService.MSG_1, 0, 0);
Message msg //把接收服务器端的回复的Messenger通过Message的replyTo参数传递给服务端
.replyTo=mRecevierReplyMsg;
msgtry{
//向服务器端发消息
.send(msg);
mService}catch (RemoteException e){
.printStackTrace();
e}
}
}
protected void onDestory(){
if(mService!=null){
unbindService(mConnection);
= null;
mService }
}
}
为了测试跨进程通信,还要对service配置android:process=":remote"
:
service android:name=".MessengerService"
< android:process=":remote"
/>
AIDL也可以进行进程间通信(IPC),由于Messenger是以串行的方式处理客户端发来的消息,效率很低,这时AIDL就可以实现并行处理消息
在Android Studio中,需要在main目录下创建一个aidl文件夹(和java文件夹同级),然后在新建和当前应用一样的包名的包,然后新建一个aidl类型的文件,内容如下(通过模板创建时可能会生成其他方法,可以删掉不要)
IMyAidl.aidl:
package com.myapp.app;
import com.myapp.app.bean.Student;
interface IMyAidl {
//自定义的远程通讯的方法
//除了基本数据类型,其他类型的参数都需要标上方向类型:in(输入), out(输出), inout(输入输出)
String addStudent(in Student student);
List<Student> getStudentAll();
}
如果通过AIDL传递对象,对象需实现Parcelable
接口(类似JAVA的Serializable
,是安卓的可持久化声明接口,但和Serializable
不同的是,还需实现一些持久化相关的方法)并放在aidl文件夹下(可以是子目录),并在与实体类同级目录下有对应的映射声明
Student.java
package com.myapp.app.bean;
import android.os.Parcel;
import android.os.Parcelable;
public class Student implements Parcelable {
private int id;
private String name;
private String className;
public Student(){}
public Student(int id, String name, String className) {
this.id = id;
this.name = name;
this.className = className;
}
protected Student(Parcel in) {
= in.readInt();
id = in.readString();
name = in.readString();
className }
public static final Creator<Student> CREATOR = new Creator<Student>() {
@Override
public Student createFromParcel(Parcel in) {
return new Student(in);
}
@Override
public Student[] newArray(int size) {
return new Student[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
.writeInt(id);
dest.writeString(name);
dest.writeString(classCame);
dest}
public void readFromParcel(Parcel source) {
= source.readInt();
id = source.readString();
name = source.readString();
className }
@Override
public String toString() {
return "Student{" +
"id='" + id + '\'' +
"name='" + name + '\'' +
"className='" + className + '\'' +
'}';
}
}
创建映射声明Student.aidl
:
package com.myapp.app.bean;
; parcelable Student
点击Make Project,Android Studio会在编译目录根据aidl文件描述生成IMyAidl.java文件,这个文件有一个Stub的静态内部类,其类型为IBinder,里面包含了我们之前在aidl文件中定义的方法,我们需要继承该类然后重写对应方法
AIDL是以服务方式提供的,所以一般会在Service中使用匿名内部类实现对应的方法,然后在onBind()
返回(Service创建在正常java代码的目录下)
public class MyAidlService extends Service {
private final String TAG = this.getClass().getSimpleName();
private ArrayList<Student> studentList = new ArrayList<>(10);
//通过匿名内部类实现之前声明的方法
private IBinder mIBinder = new IMyAidl.Stub() {
@Override
public String addStudent(Student student) throws RemoteException {
.add(student);
studentList}
@Override
public List<Student> getStudentAll() throws RemoteException {
return studentList;
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mIBinder;
}
}
在AndroidManifest.xml中声明服务
service
< android:name=".MyAidlService"
android:enabled="true"
android:exported="true"
android:process=":aidl" >
intent-filter>
<action android:name="com.myapp.app.service.MyAidlService" />
<intent-filter>
</service> </
至此服务器端已经配置完成,客户端要使用该AIDL服务,需把aidl文件夹整个复制到客户端代码中(相同位置),然后通过bind的方式启动服务
private IMyAidl aidlProxy;
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//这里拿到的不是我们继承的Stub,而是一个Proxy(代理类),函数调用是先向Service请求,得到结果再返回,但在我们看来,调用方式是一样的,我们直接调用之前声明的方法即可
= IMyAidl.Stub.asInterface(service);
aidlProxy }
@Override
public void onServiceDisconnected(ComponentName name) {
= null;
mAidl }
};
public void click(View v) {
= new Intent();
Intent intent .setAction("com.myapp.app.service.MyAidlService");
intent//使用显式intent
.setPackage("com.myapp.app.service");
intentbindService(intent, conn, BIND_AUTO_CREATE);
}
由于Service的优先级很低,所以在手机灭屏一段时间后,service很可能就被系统回收了,为了保证service不会被系统回收,我们需要将service设置为前台服务,在这个时候状态栏上会出现一个通知,通过这个通知我们可以做一些操作
startForeground(int id, Notification notification)
:该方法的作用是把当前服务设置为前台服务,其中id参数代表唯一标识通知的整型数,需要注意的是提供给
startForeground() 的整型 ID 不得为
0,而notification是一个状态栏的通知stopForeground(boolean removeNotification)
:该方法是用来从前台删除服务,此方法传入一个布尔值,指示是否也删除状态栏通知,true为删除。
注意该方法并不会停止服务。
但是,如果在服务正在前台运行时将其停止,则通知也会被删除public class ForegroundService extends Service {
private static final int NOTIFICATION_DOWNLOAD_PROGRESS_ID = 0x0001;
private boolean isRemove=false;//是否需要移除
public void createNotification(){
//使用兼容版本
.Builder builder=new NotificationCompat.Builder(this);
NotificationCompat//设置状态栏的通知图标
.setSmallIcon(R.mipmap.ic_launcher);
builder//设置通知栏横条的图标
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.screenflash_logo));
builder//禁止用户点击删除按钮删除
.setAutoCancel(false);
builder//禁止滑动删除
.setOngoing(true);
builder//右上角的时间显示
.setShowWhen(true);
builder//设置通知栏的标题内容
.setContentTitle("I am Foreground Service!!!");
builder//创建通知
Notification notification = builder.build();
//设置为前台服务
startForeground(NOTIFICATION_DOWNLOAD_PROGRESS_ID,notification);
}
public int onStartCommand(Intent intent, int flags, int startId) {
int i=intent.getExtras().getInt("cmd");
if(i==0){
if(!isRemove) {
createNotification();
}
=true;
isRemove}else {
//移除前台服务
if (isRemove) {
stopForeground(true);
}
=false;
isRemove}
return super.onStartCommand(intent, flags, startId);
}
public void onDestroy() {
//移除前台服务
if (isRemove) {
stopForeground(true);
}
=false;
isRemovesuper.onDestroy();
}
public IBinder onBind(Intent intent) {
return null;
}
显式启动
= new Intent(this,ForegroundService.class);
Intent intent startService(intent);
隐式启动(远程启动服务,即在不同的应用中启动服务,必须使用隐式启动)
final Intent serviceIntent=new Intent(); serviceIntent.setAction("com.myapp.ForegroundService");
startService(serviceIntent);
Android 5.0之后google出于安全的角度禁止了隐式声明Intent来启动Service
解决方式:
1、设置Action和packageName
final Intent serviceIntent=new Intent(); serviceIntent.setAction("com.myapp.ForegroundService");
.setPackage(getPackageName());//设置应用的包名
serviceIntentstartService(serviceIntent);
2、将隐式启动转换为显示启动
public static Intent getExplicitIntent(Context context, Intent implicitIntent) {
// Retrieve all services that can match the given intent
= context.getPackageManager();
PackageManager pm List<ResolveInfo> resolveInfo = pm.queryIntentServices(implicitIntent, 0);
// Make sure only one match was found
if (resolveInfo == null || resolveInfo.size() != 1) {
return null;
}
// Get component info and create ComponentName
= resolveInfo.get(0);
ResolveInfo serviceInfo String packageName = serviceInfo.serviceInfo.packageName;
String className = serviceInfo.serviceInfo.name;
= new ComponentName(packageName, className);
ComponentName component // Create a new intent. Use the old one for extras and such reuse
= new Intent(implicitIntent);
Intent explicitIntent // Set the component to be explicit
.setComponent(component);
explicitIntentreturn explicitIntent;
}
//调用
public void startMyService(){
=new Intent();//辅助Intent
Intent mIntent.setAction("com.myapp.ForegroundService");
mIntentfinal Intent serviceIntent=new Intent(getExplicitIntent(this,mIntent));
startService(serviceIntent);
}
静态注册:
receiver
< android:directBootAware=["true" | "false"]
android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:icon="drawable resource"
android:label="string resource"
android:name=".MyBroadcastReceiver"
android:permission="string"
android:process="string" >
intent-filter android:priority="9">
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
<intent-filter>
</intent-filter android:priority="10">
<action android:name="com.myapp.MyBroadcastReceiver">
<intent-filter>
</receiver> </
Intent.ACTION_LOCKED_BOOT_COMPLETED
,表示开机完成,但用户没有解锁手机;当用户解锁手机后,如果是前台线程,则需接收Intent.ACTION_USER_UNLOCKED
的广播,如果是后台线程,可以接收Intent.ACTION_BOOT_COMPLETED
,并可以通过UserManager.isUserUnlocked()
判断用户是否已经解锁手机;可以通过Context.moveSharedPreferencesFrom()
和Context.moveDatabaseFrom()
把数据转移到Device
encrypted storage,应用要访问Device encrypted
storage,需创建Context directBootContext = appContext.createDeviceProtectedStorageContext()
.然后directBootContext.openFileInput("filename")
动态注册(必须在退出应用时解注册,否则会导致内存泄漏):
;
MyBroadcastReceiver myBroadcastReceiver
protected void onResume(){
super.onResume();
= new MyBroadcastReceiver();
myBroadcastReceiver
= new IntentFilter();
IntentFilter intentFilter .addAction(android.net.conn.CONNECTIVITY_CHANGE);
intentFilter
//registerReceiver(myBroadcastReceiver, intentFilter);//注册广播
registerReceiver(myBroadcastReceiver, intentFilter, Manifest.permission.SEND_SMS, null);//注册带权限的广播
}
protected void onPause(){
super.onPause();
unregisterReceiver(myBroadcastReceiver);
}
不在onCreate()和onDestory() 或 onStart()和onStop()注册、注销是因为, 当系统因为内存不足时,要回收Activity占用的资源时,Activity在执行完onPause()方法后就会被销毁,有些生命周期方法onStop(),onDestory()就不会执行,当再回到此Activity时,是从onCreate方法开始执行,这时就有可能导致内存泄漏(虽然有内存泄漏的可能,但还是可以根据实际需求选择注册、解注册的时机)
JAVA代码:
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//BroadcastReceiver的onReceive生命周期很短,如果要执行>16ms的工作(无论是否开启线程),需要使用goAsync,并在完成任务时调用pendingResult.finish()
final PendingResult pendingResult = goAsync();
<String, Integer, String> asyncTask = new AsyncTask<String, Integer, String>() {
AsyncTask@Override
protected String doInBackground(String... params) {
StringBuilder sb = new StringBuilder();
.append("Action: " + intent.getAction() + "\n");
sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
sb.finish();
pendingResultreturn data;
}
};
.execute();
asyncTask}
}
广播的类型主要分为5类: - Normal Broadcast
普通广播 -
System Broadcast
系统广播 - Ordered Broadcast
有序广播 - Sticky Broadcast
粘性广播 -
Local Broadcast
App应用内广播
发送广播的时候建议:
= new Intent();
Intent intent .setAction("com.myapp.MyBroadcastReceiver");
intent.setPackage(getPackageName());//指定该广播接收器所在的包名
intent//sendBroadcast(intent);//发送广播
sendBroadcast(intent, Manifest.permission.SEND_SMS);//发送带权限的广播,此时也要求发送广播的应用在清单文件中声明此权限
除了用系统的权限,还可以使用自定义权限
<!-- 定义权限 -->
uses-permission android:name="com.myapp.permissions.MY_BROADCAST" />
<
<!-- 声明权限 -->
permission
< android:name="com.myapp.permissions.MY_BROADCAST"
android:protectionLevel="signature" >
permission> </
Intent下的常量大多都是系统广播,如Intent.ACTION_AIRPLANE_MODE_CHANGED
、Intent.ACTION_BATTERY_CHANGED
、Intent.ACTION_BOOT_COMPLETED
、Intent.ACTION_HEADSET_PLUG
、Intent.ACTION_REBOOT
等
按照android:priority的顺序接收广播,优先级高的先接收到,同样优先级的情况下,动态注册的优先;先接收到的可以对广播进行拦截、更改等操作
sendOrderedBroadcast(intent);
//BroadcastReceiver中:
abortBroadcast();//中断广播,此方法只能在接收到有序广播的时候使用,其他广播下使用会报错
由于在Android5.0(API 21)中已经失效,所以不建议使用,在这里也不作过多的总结
用于防止由于注册的广播名称相同导致的冲突问题(安全性、效率性问题),应用内广播无法跨进程通信
使用应用内广播需将静态注册的receiver的exported属性设置为false,使得广播仅在本应用(本进程)中使用
= new MyBroadcastReceiver();
MyBroadcastReceiver myBroadcastReceiver = new IntentFilter();
IntentFilter intentFilter .addAction(android.net.conn.CONNECTIVITY_CHANGE);
intentFilter
= LocalBroadcastManager.getInstance(this);
LocalBroadcastManager lbm .registerReceiver(myBroadcastReceiver, intentFilter);//注册应用内广播
lbm
= new Intent();
Intent intent .setAction("com.myapp.MyBroadcastReceiver");
intent.sendBroadcast(intent);
lbm
.unregisterReceiver(myBroadcastReceiver);//解注册应用内广播 lbm
对于不同注册方式的广播接收器回调OnReceive(Context context,Intent intent)中的context返回值是不一样的: - 对于静态注册(全局+应用内广播),回调onReceive(context, intent)中的context返回值是:ReceiverRestrictedContext - 对于全局广播的动态注册,回调onReceive(context, intent)中的context返回值是:Activity Context - 对于应用内广播的动态注册(LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Application Context - 对于应用内广播的动态注册(非LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值是:Activity Context
ContentProvider是允许不同应用进行数据交换的标准的API,它以URI(Uniform Resource Identifier
统一资源标识符)的形式对外提供数据的访问操作接口,而其他应用则通过ContentResolver根据URI去访问指定的数据;不管该应用程序是否启动,其他程序都可以通过ContentProvider来操作自己的数据接口来操作其内部的数据
URI的组成:
content://com.myapp.MyProvider/t_user/1
ˉˉˉˉˉˉˉ ˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉˉ ˉˉˉˉˉ ˉˉˉˉˉˉ
scheme authority table query
协议 授权信息 查询的表名 查询字段
配置:
provider android:authorities="list"
< android:directBootAware=["true" | "false"]
android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:grantUriPermissions=["true" | "false"]
android:icon="drawable resource"
android:initOrder="integer"
android:label="string resource"
android:multiprocess=["true" | "false"]
android:name="string"
android:permission="string"
android:process="string"
android:readPermission="string"
android:syncable=["true" | "false"]
android:writePermission="string" >
<!-- 其中exported、authorities和name都是必须配置的属性,如果配置了Permission,其他应用想要访问必须设置对应的Permission,如: -->
provider
< android:name=".MyProvider"
android:authorities="com.myapp.MyProvider"
android:exported="true"
android:readPermission="com.myapp.MyProvider.permission.READ"
provider>
> </
<!-- 其他应用中配置对应的Permission -->
uses-permission android:name="com.myapp.MyProvider.permission.READ" /> <
JAVA代码:
数据提供端
public class MyProvider extends ContentProvider{
static matcher;//常用的判断URI是否匹配的类
UriMatcher ;//ContentProvider的增删改查一般交由数据库处理
MySQLiteHelper helperstatic{
= new UriMatcher(UriMatcher.NO_MATCH);//构造函数指定没有匹配URI时的返回值
matcher .addURI("com.myapp.MyProvider", "t_user", 1);//匹配到"content://com.myapp.MyProvider/t_user"的时候返回1
matcher.addURI("com.myapp.MyProvider", "t_user/#", 2);//"#"表示任意数字
matcher}
@Override
public boolean onCreate() {
= new MySQLiteHelper(getContext());
help return false;
}
/*
根据URI返回一个表示MIME类型的字符串
返回以"vnd.android.cursor.item/"开头的字符串表示单行记录
返回以"vnd.android.cursor.dir/"开头的字符串表示多行记录
当使用
Intent intent = new Intent();
intent.setAction("com.myapp.MyActivity");
intent.setData("content://com.myapp.MyProvider/t_user");
startActivity(intent)时,会匹配到该ContentProvider,此时会调用getType,会得到MIMETYPE为"vnd.android.cursor.dir/t_user",这时如果有一个activity为
<activity
android:name=".MyActivity"
<intent-filter>
<action android:name="com.myapp.MyActivity" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.dir/t_user" />
</intent-filter>
</activity>
则会启动该activity
*/
@Override
public String getType(Uri uri) {
int code = matcher.match(uri);
String type = null;
if (code == 1) {
= "vnd.android.cursor.dir/t_user";// dir代表多行数据
type } else if (code == 2) {
= "vnd.android.cursor.item/t_user";// item单行
type }
return type;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
int code = matcher.match(uri);
Cursor cursor = null;
if (code == 1){
= help.getReadableDatabase().query( "t_user", new String[] { "id", "name", "sex" }, selection, selectionArgs, null, null, sortOrder);
cursor }else if(code == 2){
long id = ContentUris.parseId(uri);//取出"#"处的数字
= help.getReadableDatabase().query("t_user", new String[] { "id", "name", "sex" }, "id=?", new String[] { String.valueOf(id) }, null, null, sortOrder);
cursor }
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
int code = matcher.match(uri);
if (code != 1 && code != 2) {
throw new RuntimeException("地址不能匹配");
}
long id = help.getWritableDatabase().insert("t_user", null, values);
return ContentUris.withAppendedId(uri, id);// 返回值代表访问新添加数据的URI
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int code = matcher.match(uri);
if (code == 1) {
throw new RuntimeException("不能删除所有数据");
} else if (code == 2) {
long id = ContentUris.parseId(uri);
.getWritableDatabase().delete("t_user", "id=?", new String[] { id + "" });
help}
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int code = matcher.match(uri);
int row = 0;
if (code == 2) {
long id = ContentUris.parseId(uri);
= help.getWritableDatabase().update("t_user", values, "id=?", new String[] { id + "" });
row }
return row;
}
}
数据使用端(其他Activity)
= Uri.parse("content://com.myapp.MyProvider/t_user");
Uri uri Cursor cursor = getContentResolver().query(uri, null, null, null, null);
while (cursor.moveToNext()) {
int id = cursor.getInt(cursor.getColumnIndex("id"));
String name = cursor.getString(cursor.getColumnIndex("name"));
String sex = cursor.getString(cursor.getColumnIndex("sex"));
.i("TAG","id="+id+",name="+name+",sex="+sex);
Log}
.close(); cursor
使用ContentObserver
内容观察者可以监听ContentProvider相关的数据变化;如果自定义的provider想通知监听的对象(ContentObserver),需在其对应方法update
/insert/delete时,显式地调用this.getContentReslover().notifychange(uri,null)
,如果不显式调用,即使注册了ContentObserver,也不会收到回调(onChange)
public class ObserverActivity extends Activity {
;
ContentObserver observerprivate Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
.i("TAG",msg.obj.toString());
Logbreak;
default: break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.main);
= new MyObserver(this, handler);
observer //注册内容观察者,监听短信数据变化,第二个参数代表是否精确匹配,像类似带"#"或者其他查询字段的URI必须为false才能匹配到
getContentResolver().registerContentObserver(Uri.parse("content://sms"), true, observer);
}
@Override
protected void onDestory(){
//解注册
getContentResolver().unregisterContentObserver(observer);
}
}
//实现内容观察者需要继承ContentObserver
class MyObserver extends ContentObserver {
Context context;
Handler handler;
public MyObserver(Context context, Handler handler) {
super(handler);
this.context = context;
this.handler = handler;
}
//当观察的内容发生变化是会触发该方法
@Override
public void onChange(boolean selfChange) {
= context.getContentResolver();
ContentResolver resolver Cursor c = resolver.query(Uri.parse("content://sms"), null, null, null, null);
if(c != null){
StringBuilder sb = new StringBuilder();
while (c.moveToNext()) {
.append("发件人手机号码: " + c.getString(c.getColumnIndex("address"))).append("信息内容: " + c.getString(c.getColumnIndex("body"))).append("是否查看: " + c.getString(c.getColumnIndex("read"))).append("发送时间:"+ String.format("%tF %<tT", c.getLong(c.getColumnIndex("date")))).append("\n");}
sb.close();
c= new Message();
Message msg .what = 1;
msg.obj = sb.toString();//将读到的信息使用msg,传递给activity
msg.sendMessage(msg);
handler}
}
}
public class SmsObserver extends ContentObserver {
public static final String SMS_URI_INBOX = "content://sms/inbox";
private Activity activity = null;
private String smsContent = "";
private SmsListener listener;
public SmsObserver(Activity activity, Handler handler, SmsListener listener) {
super(handler);
this.activity = activity;
this.listener = listener;
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
if (uri.toString().equals("content://sms/raw")) {
return;
}
Cursor cursor = null;
//获取短信验证码
= activity.getContentResolver();
ContentResolver contentResolver = contentResolver.query(Uri.parse(SMS_URI_INBOX), new String[] { "_id", "address", "body", "read" }, "body like ? and read=?", new String[] { "%验证码%", "0" }, "date desc");
cursor if (cursor != null) {
if (cursor.moveToFirst()) {
String address = c.getString(cursor.getColumnIndex("address"));
String smsbody = cursor.getString(cursor.getColumnIndex("body"));
//如果不是我们发出的验证码则不处理
if (!address.equals("1234567890")) {
return;
}
Pattern p = Pattern.compile("(\\d{11})");
Matcher m = p.matcher(smsbody.toString());
if(matcher.find()){
= m.replaceAll("").trim().toString();
smsContent .onResult(smsContent);
listener}
}
}
}
// 短信回调接口
public interface SmsListener {
void onResult(String smsContent);
}
}
Tween动画并不会改变view的实际位置,只是把图像移动了,但原来view(比如button)的位置仍然可以点击,适用于没有点击(触摸)事件的view使用
xml标签有alpha、rotate、scale、translate,对应的JAVA类名是AlphaAnimation、RotateAnimation、ScaleAnimation、TranslateAnimation,xml中多个Tween动画(复合动画)用set标签做顶级标签,JAVA中多个Tween动画(复合动画)用AnimationSet,比如:
xml创建Tween动画
<?xml version="1.0" encoding="utf-8"?>
set xmlns:android="http://schemas.android.com/apk/res/android"
< android:interpolator="@[package:]anim/interpolator_resource"
android:shareInterpolator=["true" | "false"] >
<alpha
android:fromAlpha="float"
android:toAlpha="float" />
<!-- 可以直接写坐标,也可以用百分比,100%表示相对于自己 100%p表示相对于父布局 -->
scale
< android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float" />
translate
< android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float" />
rotate
< android:fromDegrees="float"
android:toDegrees="float"
android:pivotX="float"
android:pivotY="float" />
</set>
JAVA加载xml中的Tween动画
ImageView imageView = (ImageView) findViewById(R.id.img);
= AnimationUtils.loadAnimation(this, R.anim.anim);
Animation anim .startAnimation(anim); imageView
JAVA创建Tween动画
= (LinearLayout) findViewById(R.id.ll);
LinearLayout ll = new AnimationSet();
AnimationSet set = new AlphaAnimation(0, 1);// 0---->1从透明到不透明
AlphaAnimation alpha = new ScaleAnimation(0, 2, 0, 2);
ScaleAnimation scale = new RotateAnimation(0, 360, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
RotateAnimation rotate = new TranslateAnimation(0, 200, 0, 300);
TranslateAnimation translate .addAnimation(alpha);
set.addAnimation(scale);
set.addAnimation(rotate);
set.addAnimation(translate);
set.setDuration(3000);//统一设置动画持续时间
set.startAnimation(set); ll
像播放幻灯片一样,传一组图片进去,然后依次循环播放,可以设置每一张图片的播放时间。帧动画可以通过xml创建,也可以java代码动态构建
xml创建
<?xml version="1.0" encoding="utf-8"?>
animation-list
< xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
item android:drawable="@mipmap/run_1" android:duration="150" />
<item android:drawable="@mipmap/run_2" android:duration="150" />
<item android:drawable="@mipmap/run_3" android:duration="150" />
<item android:drawable="@mipmap/run_4" android:duration="150" />
<animation-list> </
JAVA代码创建
= new AnimationDrawable();
AnimationDrawable anim for (int i = 1; i <= 6; i++) {
int id = getResources().getIdentifier("run_" + i, "mipmap", getPackageName());
= getResources().getDrawable(id);
Drawable drawable .addFrame(drawable, 150);
anim}
.setOneShot(false);
anim.setImageDrawable(anim);
imageView.start(); anim
xml使用帧动画
ImageView>
<
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@drawable/animation_list"ImageView> </
JAVA代码使用帧动画(因为AnimationDrawable播放动画是依附在window上面的, onCreate方法中调用时Window还未初始化完毕,所以不能放在onCreate中,而应放在onWindowFocusChanged中)
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
ImageView imageView = (ImageView) findViewById(R.id.iv);
.setImageResource(R.drawable.animation_list);
imageView= (AnimationDrawable) imageView.getBackground();
AnimationDrawable anim .start();
anim//anim.stop();//停止动画
}
属性动画(ObjectAnimator
),继承自数值发生器(ValueAnimator
),可以改变view的实际参数
ValueAnimator:
= ValueAnimator.ofFloat(0, 360);
ValueAnimator animator .setDuration(1000);
animator.setInterpolator(new AccelerateInterpolator());
animator.setRepeatCount(1);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
animator@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//value就是从0到360的数字,只不过每加一延时0.36毫秒(360/1000)
float value = (float)valueAnimator.getAnimatedValue();
.setRotationY(value);
imageView}
});
.start(); animator
ObjectAnimator其实就是帮我们实现了常用动画的ValueAnimator
属性动画可以通过ObjectAnimator.ofFloat()
、ObjectAnimator.ofInt()
、ObjectAnimator.ofObject()
等静态工厂方法创建,或者通过view.animate()创建,多个属性动画(复合动画)用AnimatorSet
= new AnimatorSet();
AnimatorSet set = ObjectAnimator.ofFloat(view,"alpha",1,0,1);
ObjectAnimator alpha //JAVA代码中的单位是px,实际应用中应该根据公式转成dp
= ObjectAnimator.ofFloat(view,"rotationX",0,270,50);
ObjectAnimator rotationX = ObjectAnimator.ofFloat(tv, "translationX", 0, 200, -200,0);
ObjectAnimator translationX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 2f, 1f);
ObjectAnimator scaleX
.play(translationX).before(alpha);//先水平移动
set.play(alpha).with(rotationX);//然后透明变换和旋转同时执行
set.play(scaleX).after(alpha);//最后执行缩放动画
set.setRepeatCount(-1);//-1表示一直重复
set.setDuration(2000);//动画持续2秒
set.setStartDelay(1000);//延迟1秒执行
set.start();
set
//通过view.animate()创建
.animate().translationXBy(view.getWidth())
view.translationYBy(view.getWidth())
.setDuration(2000)
.setInterpolator(new BounceInterpolator())
.start();
//除了使用系统提供的变换方式,还可以自己指定动画的移动路径
= new Path();
Path path .cubicTo(0.2f, 0f, 0.1f, 1f, 0.5f, 1f);
path.lineTo(1f, 1f);
path= ObjectAnimator.ofFloat(iv, view.X, view.Y, path);
ObjectAnimator animator .setDuration(2000);
animator.setRepeatCount(1);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.start(); animator
插值器(Interpolator
)可以用来控制动画过程的变化速率
xml中使用:
<!--
下面是xml中所有的interpolator,对应的JAVA类就是把下划线去掉,单词首字母大写
@android:anim/accelerate_interpolator 设置动画为加速动画(动画播放中越来越快)
@android:anim/decelerate_interpolator 设置动画为减速动画(动画播放中越来越慢)
@android:anim/accelerate_decelerate_interpolator 设置动画为先加速在减速(开始速度最快 逐渐减慢)
@android:anim/anticipate_interpolator 先反向执行一段,然后再加速反向回来(相当于我们弹簧,先反向压缩一小段,然后在加速弹出)
@android:anim/anticipate_overshoot_interpolator 同上先反向一段,然后加速反向回来,执行完毕自带回弹效果(更形象的弹簧效果)
@android:anim/bounce_interpolator 执行完毕之后会回弹跳跃几段(相当于我们高空掉下一颗皮球,到地面是会跳动几下)
@android:anim/cycle_interpolator 循环,动画循环一定次数,值的改变为一正弦函数:Math.sin(2* mCycles* Math.PI* input)
@android:anim/linear_interpolator 线性均匀改变
@android:anim/overshoot_interpolator 加速执行,结束之后回弹
-->
set android:interpolator="@android:anim/accelerate_interpolator">
<<!-- xxx -->
set> </
JAVA中使用:
.setInterpolator(new AccelerateInterpolator());
animation
//除了xml中对应的Interpolator类,还有JAVA代码特有的PathInterpolator
//PathInterpolator是按照贝塞尔曲线运动的,官方提供了一个工具类生成PathInterpolator,这样就不用自己实现贝塞尔曲线了
//三阶贝塞尔曲线
= new Path();
Path path .cubicTo(0.2f, 0f, 0.1f, 1f, 0.5f, 1f);
path.lineTo(1f, 1f);
path
= ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 500);
ObjectAnimator animator .setInterpolator(PathInterpolatorCompat.create(path));//通过工具类创建
animator.start(); animator
通过overridePendingTransition指定Activity转场动画
<?xml version="1.0" encoding="utf-8"?>
set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%"
< android:toXDelta="0%"
android:duration="500" />
set> </
<?xml version="1.0" encoding="utf-8"?>
set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0%"
< android:toXDelta="-40%"
android:duration="500" />
set> </
startActivity(new Intent(this, Activity2.class));
overridePendingTransition(R.anim.slide_right_in, R.anim.slide_left_out);
Android5.0新增的转场动画 - Explode - Slide - Fade - Share
public class CActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//设置Explode进场动画
//getWindow().setEnterTransition(new Explode());
//设置Slide进场动画
//getWindow().setEnterTransition(new Slide());
//设置Fade进场、出场动画
getWindow().setEnterTransition(new Fade());
getWindow().setExitTransition(new Fade());
setContentView(R.layout.activity_c);
}
}
share动画:
<!-- 首先,两个Activity共享的元素需要设置相同的transitionName: android:transitionName="fab" -->
Button
< android:id="@+id/fab_button"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@mipmap/ic_launcher"
android:elevation="5dp"
android:onClick="explode"
android:transitionName="fab" />
Button
< android:id="@+id/fab_button"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_alignParentEnd="true"
android:layout_below="@id/holder_view"
android:layout_marginTop="-80dp"
android:background="@mipmap/ic_launcher"
android:elevation="5dp"
android:transitionName="fab" />
// 跳转时,要为每一个共享的view设置对应的transitionName
View fab = findViewById(R.id.fab_button);
View txName = findViewById(R.id.tx_user_name);
= new Intent(this, CActivity.class);
intent startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this,
.create(view, "share"),
Pair.create(fab, "fab"),
Pair.create(txName, "user_name"))
Pair.toBundle());
// 跳转的Activity在onCreate方法中开启Transition模式
public class CActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
setContentView(R.layout.activity_c);
}
}
使用5.0新增的动画要在startActivity的时候在第二个参数指定Bundle,固定写法为ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
= new Intent(this, CActivity.class);
Intent intent startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
这是Android5.0推出的新的动画框架,可以给View做一个圆角的揭露效果
//五个参数分别是View,中心点坐标x、y,开始半径,结束半径
= ViewAnimationUtils.createCircularReveal(view, 0, 0, 0, (float) Math.hypot(rect.getWidth(), rect.getHeight()));
Animator anim .setDuration(2000);
anim.start(); anim
SVG就是标准的矢量图格式,xml中定义的path就是用了SVG的命令,SVG常用命令有
首先创建一个ImageView
ImageView
< android:id="@+id/imgBtn"
android:layout_width="200dp"
android:layout_height="200dp"
android:onClick="startAnim"
android:src="@drawable/animvectordrawable" />
src指向的是一个animated-vector
,animated-vector
的drawable指定静态时显示的图案,target
是对外部xml的引用,target
(objectAnimator
)的name对应vector
中的name,用于指定图像的哪一部分做哪种动画
<!-- animvectordrawable.xml -->
animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
< android:drawable="@drawable/vectordrawable" >
target
< android:name="rotationGroup"
android:animation="@anim/rotation" />
target
< android:name="v"
android:animation="@anim/path_morph" />
animated-vector> </
第一个target
指向的是一个objectAnimator
,objectAnimator
表示动画的变换方式
<!-- rotation.xml -->
objectAnimator
< xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1500"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360" />
第二个target
指向的是一个set
,其中valueFrom、valueTo用的就是SVG命令
<!-- path_morph.xml -->
set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
< android:duration="1500"
android:propertyName="pathData"
android:valueFrom="M10,10 L60,10 L60,20 L10,20 Z M10,30 L60,30 L60,40 L10,40 Z M10,50 L60,50 L60,60 L10,60 Z"
android:valueTo="M5,35 L40,0 L47.072,7.072 L12.072,42.072 Z M10,30 L60,30 L60,40 L10,40 Z M12.072,27.928 L47.072,62.928 L40,70 L5,35 Z"
android:valueType="pathType" />
set> </
animated-vector
的drawable指向的是一个vector
,vector
是元素的矢量资源(静态时显示的图案),animated-vector
规定,可以有多个动画同时进行,但是一个对象上只能加载一个动画,这里既做旋转又做path变换,就需要用group标签,把path变换动画放在path对象上,把旋转动画放在group对象上,从而实现整体的效果
<!-- vectordrawable.xml -->
vector xmlns:android="http://schemas.android.com/apk/res/android"
< android:height="300dp"
android:width="300dp"
android:viewportHeight="70"
android:viewportWidth="70" >
group
< android:name="rotationGroup"
android:pivotX="35"
android:pivotY="35"
android:rotation="0.0" >
path
< android:name="v"
android:fillColor="#000000"
android:pathData="M10,10 L60,10 L60,20 L10,20 Z M10,30 L60,30 L60,40 L10,40 Z M10,50 L60,50 L60,60 L10,60 Z" />
group>
</vector> </
xml设置好后就可以在JAVA代码中启动动画了
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void startAnim(View view) {
= imgBtn.getDrawable();
Drawable drawable ((Animatable) drawable).start();
}
但是这时只能从一个图像变换成另一个图像,并不能变回来,这时就需要使用animated-selector
这里两个item
分别表示两个状态(state_checked为true和默认状态),指向两个vector
;两个transition
分别指定两种状态之间互换的过渡动画,指向两个animated-vector
<?xml version="1.0" encoding="utf-8"?>
animated-selector
< xmlns:android="http://schemas.android.com/apk/res/android">
item android:id="@+id/state_on"
< android:drawable="@drawable/ic_twitter"
android:state_checked="true"/>
item android:id="@+id/state_off"
< android:drawable="@drawable/ic_heart" />
transition
< android:fromId="@id/state_off"
android:toId="@id/state_on"
android:drawable="@drawable/avd_heart_to_twitter" />
transition
< android:fromId="@id/state_on"
android:toId="@id/state_off"
android:drawable="@drawable/avd_twitter_to_heart" />
animated-selector> </
transition
指向的其中一个animated-vector
,这里指定了两个动画,一个是旋转动画,另一个是path动画
<?xml version="1.0" encoding="utf-8"?>
animated-vector
< xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:drawable="@drawable/ic_heart">
target android:name="groupHeart">
<aapt:attr name="android:animation">
<objectAnimator
< android:propertyName="rotation"
android:valueFrom="-360"
android:valueTo="0"
android:duration="1000" />
aapt:attr>
</target>
</
target android:name="heart">
<aapt:attr name="android:animation">
<objectAnimator
< android:duration="1000"
android:propertyName="pathData"
android:valueFrom="M 12.0,21.35 l -1.45,-1.32 C 5.4,15.36,2.0,12.28,2.0,8.5 C 2.0,5.42,4.42,3.0,7.5,3.0 c 1.74,0.0,3.41,0.81,4.5,2.09 C 13.09,3.81,14.76,3.0,16.5,3.0 C 19.58,3.0,22.0,5.42,22.0,8.5 c 0.0,3.78,-3.4,6.86,-8.55,11.54 L 12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 C 12.0,21.35,12.0,21.35,12.0,21.35 L 12.0,21.35"
android:valueTo="M 22.46,6.0 l 0.0,0.0 C 21.69,6.35,20.86,6.58,20.0,6.69 C 20.88,6.16,21.56,5.32,21.88,4.31 c 0.0,0.0,0.0,0.0,0.0,0.0 C 21.05,4.81,20.13,5.16,19.16,5.36 C 18.37,4.5,17.26,4.0,16.0,4.0 c 0.0,0.0,0.0,0.0,0.0,0.0 L 16.0,4.0 C 13.65,4.0,11.73,5.92,11.73,8.29 C 11.73,8.63,11.77,8.96,11.84,9.27 C 8.28,9.09,5.11,7.38,3.0,4.79 C 2.63,5.42,2.42,6.16,2.42,6.94 C 2.42,8.43,3.17,9.75,4.33,10.5 C 3.62,10.5,2.96,10.3,2.38,10.0 C 2.38,10.0,2.38,10.0,2.38,10.03 C 2.38,12.11,3.86,13.85,5.82,14.24 C 5.46,14.34,5.08,14.39,4.69,14.39 C 4.42,14.39,4.15,14.36,3.89,14.31 C 4.43,16.0,6.0,17.26,7.89,17.29 C 6.43,18.45,4.58,19.13,2.56,19.13 C 2.22,19.13,1.88,19.11,1.54,19.07 C 3.44,20.29,5.7,21.0,8.12,21.0 C 16.0,21.0,20.33,14.46,20.33,8.79 C 20.33,8.6,20.33,8.42,20.32,8.23 C 21.16,7.63,21.88,6.87,22.46,6.0 L 22.46,6.0"
android:valueType="pathType" />
aapt:attr>
</target>
</animated-vector> </
[onInflate()
->]onAttach()
->onCreate()
->onCreateView()
->onViewCreated()
->onActivityCreated()
->onViewStateRestored()
->onStart()
->onResume()
->onCreateOptionsMenu()
->onPrepareOptionsMenu()
->Fragment运行->onPause()
->onSaveInstanceState()
->onStop()
->onDestoryView()
->onDestory()
->onDetach()
JAVA代码:
public class MyFragment extends Fragment {
@Override
public void onAttach(Context context){
super.onAttach(context);
//Activity之间传递参数可以通过getIntent().getExtras()
//而Fragment之间传递参数可以使用getArguments()
= getArguments();
Bundle bundle }
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
//通过Inflater加载布局R.layout.fragment1和普通activity的布局文件相同,只不过变成了由fragment来加载
View view = Inflater.inflate(R.layout.fragment1, container, false);
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
//初始化布局、参数
//view.findViewById(...)
}
}
xml使用自己的fragment(通过name设置):
<?xml version="1.0" encoding="utf-8"?>
LinearLayout
< xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
fragment
< android:id="@+id/fragment1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:name="com.myapp.MyFragment"/>
LinearLayout> </
= getFragmentManager();// v4中,getSupportFragmentManager
FragmentManager frgmentManager = frgmentManager.benginTransatcion();
FragmentTransaction transaction .addToBackStack(null);//支持返回键,点击返回时,回到上一个frgment,否则点返回直接退出app
transaction.add(R.id.containerViewId, fragment1);//添加,containerView一般是FrameLayout
transaction//如果在FragmentA中的EditText填了一些数据,当切换到FragmentB时,如果希望会到A还能看到数据,就用add加hide和show,如果不需要保存用户操作,就用add和remove
.hide(fragment1);//隐藏,搭配add使用
transaction.show(fragment1);//显示,搭配add使用
transaction.setBreadCrumbShortTitle(R.string.title);//添加标题
transaction
//transaction.replace(R.id.containerViewId, fragment2);//替换,和add、remove一样,会销毁布局容器内的已有视图,这样会导致每次切换Fragment时都会重新初始化,类似EditText中已输入的数据不会被保留
//在不考虑回退栈的情况下,remove会销毁整个Fragment实例,而detach则只是销毁其视图结构,实例并不会被销毁,如果当前Activity一直存在,那么在不希望保留用户操作的时候,你可以优先使用detach
//transaction.remove(fragment2);//删除
//transaction.detach(fragment2);//取消挂载
.commit();//提交后才生效
transaction
//可以手动返回上一个fragment
getFragmentManager().popBackStack();
如果使用add加hide和show,在旋转屏幕、从后台回来、使用Instant Run调试等情况下,会出现重叠现象,这是因为当Activity被销毁再重新创建时(或其他情况导致再次调用onCreate时),onCreate被再次调用,重新初始化了Fragment并add了两个新的Fragment实例(当在onCreate中new Fragment并add时),当点击按钮切换Fragment时,hide或show的实例是新创建的实例,这就导致了旧的Fragment没有被正确地hide,解决办法是
方法1:在add的时候添加tag,如onCreate被再次调用,就从FragmentManager中取回之前创建的Fragment
;
Fragment1 fragment1;
Fragment2 fragment2
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
= getSupportFragmentManager();
FragmentManager fragmentManager
//防止fragment产生重叠问题
if (savedInstanceState == null) {
= new Fragment1();
fragment1 = new Fragment2();
fragment2
.beginTransaction()
fragmentManager.add(R.id.containerViewId, fragment1, Fragment1.class.getName())
.add(R.id.containerViewId, fragment2, Fragment2.class.getName())
.commit();
} else {
= (Fragment1) fragmentManager.findFragmentByTag(Fragment1.class.getName());
fragment1 = (Fragment2) fragmentManager.findFragmentByTag(Fragment2.class.getName());
fragment2 }
.beginTransaction()
fragmentManager.show(fragment1)
.hide(fragment2)
.commit();
}
最好在Fragment中再判断一次Parent是否为空
private View mRoot;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if(mRoot==null){
= Inflater.inflate(R.layout.fragment1, container, false);
mRoot } else if(mRoot.getParent!=null){
((ViewGroup)mRoot.getParent()).removeView(mRoot);
}
return mRoot;
}
方法2:直接禁止保存视图及相关数据,每次onCreate都全部重新初始化。当切换到其他app时,Activity被销毁并通过onSaveInstanceState
保存Fragment及其他视图相关的数据,如果把onSaveInstanceState
改成空的函数,则Activity被销毁时不会保存任何东西,这样之前add的Fragment也会被清空
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
//super.onSaveInstanceState(outState);
}
当使用ViewPager+Fragment时,由于ViewPager一开始只加载前两页,而且默认同时加载三页(可以通过setOffscreenPageLimit(int limit)
更改),多余的实例会销毁,这时懒加载就很有必要了。懒加载可以让当fragment对用户可见时才开始加载,而ViewPager中对用户不可见的页不会进行加载
在Fragment中有一个setUserVisibleHint的方法,这个方法是优于onCreate方法的,所以也可以作为Fragment的一个生命周期来看待,它会通过isVisibleToUser告诉我们当前Fragment我们是否可见,我们可以在可见的时候再进行加载(比如网络请求)
//当该页面对用户可见/不可见时,系统都会回调此方法
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
if (isVisibleToUser) {
//网络请求等操作
}
super.setUserVisibleHint(isVisibleToUser);
}
网络请求后一般要进行UI更新,但这时由于setUserVisibleHint
先于onCreate
调用,UI还没加载完,无法在setUserVisibleHint更新UI,所以一般在onViewCreated添加完成UI加载的标志,再根据getUserVisibleHint()
判断用户是否可见,同时满足两个条件才开始网络请求
private boolean isViewCreated;//Fragment的View加载完毕的标记
private boolean isLoadingData;//是否正在加载数据,防止UI加载好并且对用户可见,开始了一次网络请求但还没完成请求时,重复调用lazyLoad后导致的重复请求
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
= true;
isViewCreated lazyLoad();
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
lazyLoad();
}
}
private void lazyLoad() {
//只有当view创建好并且对用户可见,而且没有正在加载数据时才开始加载数据
if (isViewCreated && getUserVisibleHint() && !isLoadingData) {
= true;
isLoadingData // TODO 网络请求等操作,一般新建线程来做
//数据加载完毕,恢复标记,防止重复加载
= false;
isViewCreated = false;
isLoadingData }
}
xml设置横竖屏
android:screenOrientation="portrait"
screenOrientation可选的值有: - unspecified 默认值,由系统判断状态自动切换 - landscape 横屏 - portrait 竖屏 - user 用户当前设置的orientation值 - behind 下一个要显示的Activity的orientation值 - sensor 使用传感器 用传感器的方向 - nosensor 不使用传感器 基本等同于unspecified
java设置横竖屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
//获取横竖屏状态
int screenNum = getResources().getConfiguration().orientation;
横竖屏切换时,会调用Activity的生命周期,导致组件内容会被重置(如EditText中的内容),如果不想被重置,需在清单文件中的activity中添加configChanges,并至少配置orientation|keyboardHidden|screenSize
这三个属性,才能使切屏时Activity的生命周期不会被调用,只会执行onConfigurationChanged方法(只设置orientation
不起作用)
android:configChanges="orientation|keyboardHidden|screenSize"
configChanges的可选值有: - orientation 屏幕在纵向和横向间旋转 - keyboardHidden 键盘显示或隐藏 - screenSize 屏幕大小改变了 - fontScale 用户变更了首选的字体大小 - locale 用户选择了不同的语言设定 - keyboard 键盘类型变更,例如手机从12键盘切换到全键盘 - touchscreen或navigation 键盘或导航方式变化,一般不会发生这样的事件
@Override
public void onConfigurationChanged(Configuration newConfig){
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
//当前屏幕为横屏
} else {
//当前屏幕为竖屏
}
super.onConfigurationChanged(newConfig);
}
如果不设置android:configChanges
,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次
如果只设置了android:configChanges="orientation"
,横竖屏切换后,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次
先准备一个html(myweb.html)
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<script type="text/javascript">
function actionFromJAVA(){
document.getElementById("log_msg").innerHTML +=
"<br\>JAVA调用了js函数";
}
function actionFromJAVAWithParam(arg){
document.getElementById("log_msg").innerHTML +=
"<br\>JAVA调用了js函数并传递参数:"+arg);
(
}
</script>
</head>
<body>
<p>WebView与Javascript交互</p>
<div>
<button onClick="window.myweb.actionFromJs()">点击调用JAVA代码</button>
</div>
<br/>
<div>
<button onClick="window.myweb.actionFromJsWithParam('come from Js')">点击调用JAVA代码并传递参数</button>
</div>
<br/>
<div id="log_msg">调用打印信息</div>
</body>
</html>
再准备一个WebView,JAVA调javascript只需要用Webview.loadUrl(),URL格式为”javascript:js中的函数名”
private WebView webView;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_web_view);
= (WebView) findViewById(R.id.web_view);
webView= (Button) findViewById(R.id.bt_button);
button
= webView.getSettings();
WebSettings webSettings .setJavaScriptEnabled(true);//支持javaScript
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);//无缓存
webSettings.setDomStorageEnabled(true);//支持H5 如果不设置部分url加载会出现空白
webSettings.setSupportZoom(true);//支持缩放,默认为true。是下面那个的前提。
webSettings.setBuiltInZoomControls(true);//设置内置的缩放控件。若为false,则该WebView不可缩放
webSettings.setDisplayZoomControls(false);//隐藏原生的缩放控件
webSettings
//使得打开网页时不调用系统浏览器, 而是在本WebView中显示
.setWebViewClient(new WebViewClient(){
webView@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
.loadUrl(url);
viewreturn true;
}
});
//文件放在src/main/assets/下时,URL为file:///android_asset/xxx
.loadUrl("file:///android_asset/myweb.html");
webView.setOnClickListener(new View.OnClickListener() {
button@Override
public void onClick(View v) {
//无参数调用javascript的函数
.loadUrl("javascript:actionFromJAVA()");
webView
String param = "JAVA";
//有参数调用javascript的函数
.loadUrl("javascript:actionFromJAVAWithParam(" + "\'" + param + "\'" + ")");
webView}
});
}
javascript调JAVA需要为webView设置JavaScriptInterface
//第二个参数表示标识,javascript调JAVA函数时方法名前缀就是这个标识,如window.myweb.actionFromJs(),最前面的window是固定写法,然后是标识,最后才是JAVA的方法名,该方法就是第一个参数的Object中实现了的方法,且此方法要有@JavascriptInterface注解
.addJavascriptInterface(new MyObject(),"myweb");
webView//要先addJavascriptInterface再loadUrl,JavascriptInterface才会生效
.loadUrl("file:///android_asset/myweb.html");
webView
class MyObject{
@JavascriptInterface
actionFromJs(){
.i("TAG", "javascript调用了JAVA的函数");
Log//该函数不一定是主线程调,更新UI应该用runOnUiThread
runOnUiThread(new Runnable() {
public void run() {
.makeText(MyActivity.this, "js调用了Native函数", Toast.LENGTH_SHORT).show();
Toast}
});
}
@JavascriptInterface
actionFromJsWithParam(String arg){
.i("TAG", "javascript调用了JAVA的函数,参数是"+arg);
Log}
}
浏览器跳转app通过URI实现,其中scheme和host是必须的,其他可以省略
myapp://abcdefg.me/fromweb?data=4
ˉˉˉˉˉ ˉˉˉˉˉˉˉˉˉ ˉˉˉˉˉˉˉ ˉˉˉˉˉ
scheme host path query
html代码
<a href="myapp://abcdefg.me/fromweb?data=4">启动应用程序</a>
AndroidManifest.xml配置,通过data的scheme和host匹配(必须有这两个属性),如果设置了pathPrefix,再根据pathPrefix匹配(可选)
intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
< android:host="abcdefg.me"
android:scheme="myapp"
android:pathPrefix="/fromweb"
/>intent-filter> </
JAVA通过getIntent().getData
得到URI
= getIntent();
Intent intent String action = intent.getAction();
if(Intent.ACTION_VIEW.equals(action)){
= intent.getData();
Uri uri if(uri != null){
String data = uri.getQueryParameter("data");
}
}
官方的夜间模式支持只能在Android 6.0以上使用
dependencies {
compile 'com.android.support:appcompat-v7:23.4.0'
}
DayNight
style name="AppTheme" parent="Theme.AppCompat.DayNight">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<style> </
colors
和style
在res
下新建values-night
文件夹,当切换夜间主题时,会读取values-night
中的style.xml
或colors.xml
在app初始化或者Application中用设置
.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO); AppCompatDelegate
在应用中设置,Activity必须是继承自AppCompatActivity
的,设置完需重启Activity
//获取当前模式
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
//切换模式
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
//重启Activity,也可以用startActivity的方式重启
recreate();
有四个可选的模式
调整完日间/夜间模式后,必须重启Activity,这时可以使用动画来解决闪屏的问题
//这里不使用recreate的方式重启
finish();
overridePendingTransition(android.R.anim.fade_in,android.R.anim.fade_out);
startActivity(this.getIntent());
要执行的动作,系统提供的有: - Activity - ACTION_MAIN - ACTION_VIEW - ACTION_ATTACH_DATA - ACTION_EDIT - ACTION_PICK - ACTION_CHOOSER - ACTION_GET_CONTENT - ACTION_DIAL - ACTION_CALL - ACTION_SEND - ACTION_SENDTO - ACTION_ANSWER - ACTION_INSERT - ACTION_DELETE - ACTION_RUN - ACTION_SYNC - ACTION_PICK_ACTIVITY - ACTION_SEARCH - ACTION_WEB_SEARCH - ACTION_FACTORY_TEST - Broadcast - ACTION_TIME_TICK - ACTION_TIME_CHANGED - ACTION_TIMEZONE_CHANGED - ACTION_BOOT_COMPLETED - ACTION_PACKAGE_ADDED - ACTION_PACKAGE_CHANGED - ACTION_PACKAGE_REMOVED - ACTION_PACKAGE_RESTARTED - ACTION_PACKAGE_DATA_CLEARED - ACTION_PACKAGES_SUSPENDED - ACTION_PACKAGES_UNSUSPENDED - ACTION_UID_REMOVED - ACTION_BATTERY_CHANGED - ACTION_POWER_CONNECTED - ACTION_POWER_DISCONNECTED - ACTION_SHUTDOWN
也可以自定义动作
//Intent intent = new Intent("com.myapp.intent.MY_ACTION");
//也可以写成
= new Intent();
Intent intent .setAction("com.myapp.intent.MY_ACTION");
intentstartActivity(intent);
这时就会寻找intent-filter
中带有此action的activity:
activity android:name=".MyActivity">
<intent-filter>
<action android:name="com.myapp.intent.MY_ACTION"/>
<category android:name="android.intent.category.DEFAULT"/>
<intent-filter>
</activity> </
系统提供的有: - CATEGORY_DEFAULT - CATEGORY_BROWSABLE - CATEGORY_TAB - CATEGORY_ALTERNATIVE - CATEGORY_SELECTED_ALTERNATIVE - CATEGORY_LAUNCHER - CATEGORY_INFO - CATEGORY_HOME - CATEGORY_PREFERENCE - CATEGORY_TEST - CATEGORY_CAR_DOCK - CATEGORY_DESK_DOCK - CATEGORY_LE_DESK_DOCK - CATEGORY_HE_DESK_DOCK - CATEGORY_CAR_MODE - CATEGORY_APP_MARKET - CATEGORY_VR_HOME
在intent中指定category:
= new Intent(); intent.setAction(Intent.ACTION_MAIN);// 添加Action属性 intent.addCategory(Intent.CATEGORY_HOME);// 添加Category属性
Intent intent startActivity(intent);// 启动Activity
在intent-filter
中配置category:
activity android:name=".MyActivity">
<intent-filter>
<action android:name="android.intent.action.ACTION_MAIN"/>
<category android:name="android.intent.category.CATEGORY_HOME"/>
<intent-filter>
</activity> </
执行动作所需的数据
用浏览器打开指定网页:
= new Intent(Intent.ACTION_VIEW);
Intent intent .setData(Uri.parse("http://www.baidu.com"));
intentstartActivity(intent);
打开系统拨号界面并指定电话号码:
= new Intent(Intent.ACTION_CALL);
Intent intent .setData(Uri.parse("tel:12345678"));
intentstartActivity(intent);
支持浏览器打开本应用时,也要指定data:
intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
< android:scheme="myapp"
android:host="abcdefg.me"
android:port="8080"
android:pathPrefix="/fromweb"
/>intent-filter> </
模仿浏览器打开应用
= new Intent(Intent.ACTION_VIEW);
Intent intent .setData(Uri.parse("myapp://abcdefg.me:8080/fromweb/path1?data=213"));
intentstartActivity(intent);
执行动作所需的附加信息
用浏览器搜索指定内容:
= new Intent(Intent.ACTION_WEB_SEARCH);
Intent intent .putExtra(SearchManager.QUERY, "android");
intentstartActivity(intent);
通过MIME调用其他应用打开对应格式的文件
= new Intent();
Intent intent .setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "video/x-msvideo");
intentstartActivity(intent);
应用指定支持的type:
intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<intent-filter> </
MIME大全
final static String[][] MIME_MapTable = {
{ ".323", "text/h323" },
{ ".3gp", "video/3gpp" },
{ ".aab", "application/x-authoware-bin" },
{ ".aam", "application/x-authoware-map" },
{ ".aas", "application/x-authoware-seg" },
{ ".acx", "application/internet-property-stream" },
{ ".ai", "application/postscript" },
{ ".aif", "audio/x-aiff" },
{ ".aifc", "audio/x-aiff" },
{ ".aiff", "audio/x-aiff" },
{ ".als", "audio/X-Alpha5" },
{ ".amc", "application/x-mpeg" },
{ ".ani", "application/octet-stream" },
{ ".apk", "application/vnd.android.package-archive" },
{ ".asc", "text/plain" },
{ ".asd", "application/astound" },
{ ".asf", "video/x-ms-asf" },
{ ".asn", "application/astound" },
{ ".asp", "application/x-asap" },
{ ".asr", "video/x-ms-asf" },
{ ".asx", "video/x-ms-asf" },
{ ".au", "audio/basic" },
{ ".avb", "application/octet-stream" },
{ ".avi", "video/x-msvideo" },
{ ".awb", "audio/amr-wb" },
{ ".axs", "application/olescript" },
{ ".bas", "text/plain" },
{ ".bcpio", "application/x-bcpio" },
{ ".bin ", "application/octet-stream" },
{ ".bld", "application/bld" },
{ ".bld2", "application/bld2" },
{ ".bmp", "image/bmp" },
{ ".bpk", "application/octet-stream" },
{ ".bz2", "application/x-bzip2" },
{ ".c", "text/plain" },
{ ".cal", "image/x-cals" },
{ ".cat", "application/vnd.ms-pkiseccat" },
{ ".ccn", "application/x-cnc" },
{ ".cco", "application/x-cocoa" },
{ ".cdf", "application/x-cdf" },
{ ".cer", "application/x-x509-ca-cert" },
{ ".cgi", "magnus-internal/cgi" },
{ ".chat", "application/x-chat" },
{ ".class", "application/octet-stream" },
{ ".clp", "application/x-msclip" },
{ ".cmx", "image/x-cmx" },
{ ".co", "application/x-cult3d-object" },
{ ".cod", "image/cis-cod" },
{ ".conf", "text/plain" },
{ ".cpio", "application/x-cpio" },
{ ".cpp", "text/plain" },
{ ".cpt", "application/mac-compactpro" },
{ ".crd", "application/x-mscardfile" },
{ ".crl", "application/pkix-crl" },
{ ".crt", "application/x-x509-ca-cert" },
{ ".csh", "application/x-csh" },
{ ".csm", "chemical/x-csml" },
{ ".csml", "chemical/x-csml" },
{ ".css", "text/css" },
{ ".cur", "application/octet-stream" },
{ ".dcm", "x-lml/x-evm" },
{ ".dcr", "application/x-director" },
{ ".dcx", "image/x-dcx" },
{ ".der", "application/x-x509-ca-cert" },
{ ".dhtml", "text/html" },
{ ".dir", "application/x-director" },
{ ".dll", "application/x-msdownload" },
{ ".dmg", "application/octet-stream" },
{ ".dms", "application/octet-stream" },
{ ".doc", "application/msword" },
{ ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
{ ".dot", "application/msword" },
{ ".dvi", "application/x-dvi" },
{ ".dwf", "drawing/x-dwf" },
{ ".dwg", "application/x-autocad" },
{ ".dxf", "application/x-autocad" },
{ ".dxr", "application/x-director" },
{ ".ebk", "application/x-expandedbook" },
{ ".emb", "chemical/x-embl-dl-nucleotide" },
{ ".embl", "chemical/x-embl-dl-nucleotide" },
{ ".eps", "application/postscript" },
{ ".epub", "application/epub+zip" },
{ ".eri", "image/x-eri" },
{ ".es", "audio/echospeech" },
{ ".esl", "audio/echospeech" },
{ ".etc", "application/x-earthtime" },
{ ".etx", "text/x-setext" },
{ ".evm", "x-lml/x-evm" },
{ ".evy", "application/envoy" },
{ ".exe", "application/octet-stream" },
{ ".fh4", "image/x-freehand" },
{ ".fh5", "image/x-freehand" },
{ ".fhc", "image/x-freehand" },
{ ".fif", "application/fractals" },
{ ".flr", "x-world/x-vrml" },
{ ".flv", "flv-application/octet-stream" },
{ ".fm", "application/x-maker" },
{ ".fpx", "image/x-fpx" },
{ ".fvi", "video/isivideo" },
{ ".gau", "chemical/x-gaussian-input" },
{ ".gca", "application/x-gca-compressed" },
{ ".gdb", "x-lml/x-gdb" },
{ ".gif", "image/gif" },
{ ".gps", "application/x-gps" },
{ ".gtar", "application/x-gtar" },
{ ".gz", "application/x-gzip" },
{ ".h", "text/plain" },
{ ".hdf", "application/x-hdf" },
{ ".hdm", "text/x-hdml" },
{ ".hdml", "text/x-hdml" },
{ ".hlp", "application/winhlp" },
{ ".hqx", "application/mac-binhex40" },
{ ".hta", "application/hta" },
{ ".htc", "text/x-component" },
{ ".htm", "text/html" },
{ ".html", "text/html" },
{ ".hts", "text/html" },
{ ".htt", "text/webviewhtml" },
{ ".ice", "x-conference/x-cooltalk" },
{ ".ico", "image/x-icon" },
{ ".ief", "image/ief" },
{ ".ifm", "image/gif" },
{ ".ifs", "image/ifs" },
{ ".iii", "application/x-iphone" },
{ ".imy", "audio/melody" },
{ ".ins", "application/x-internet-signup" },
{ ".ips", "application/x-ipscript" },
{ ".ipx", "application/x-ipix" },
{ ".isp", "application/x-internet-signup" },
{ ".it", "audio/x-mod" },
{ ".itz", "audio/x-mod" },
{ ".ivr", "i-world/i-vrml" },
{ ".j2k", "image/j2k" },
{ ".jad", "text/vnd.sun.j2me.app-descriptor" },
{ ".jam", "application/x-jam" },
{ ".jar", "application/java-archive" },
{ ".java", "text/plain" },
{ ".jfif", "image/pipeg" },
{ ".jnlp", "application/x-java-jnlp-file" },
{ ".jpe", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
{ ".jpg", "image/jpeg" },
{ ".jpz", "image/jpeg" },
{ ".js", "application/x-javascript" },
{ ".jwc", "application/jwc" },
{ ".kjx", "application/x-kjx" },
{ ".lak", "x-lml/x-lak" },
{ ".latex", "application/x-latex" },
{ ".lcc", "application/fastman" },
{ ".lcl", "application/x-digitalloca" },
{ ".lcr", "application/x-digitalloca" },
{ ".lgh", "application/lgh" },
{ ".lha", "application/octet-stream" },
{ ".lml", "x-lml/x-lml" },
{ ".lmlpack", "x-lml/x-lmlpack" },
{ ".log", "text/plain" },
{ ".lsf", "video/x-la-asf" },
{ ".lsx", "video/x-la-asf" },
{ ".lzh", "application/octet-stream" },
{ ".m13", "application/x-msmediaview" },
{ ".m14", "application/x-msmediaview" },
{ ".m15", "audio/x-mod" },
{ ".m3u", "audio/x-mpegurl" },
{ ".m3url", "audio/x-mpegurl" },
{ ".m4a", "audio/mp4a-latm" },
{ ".m4b", "audio/mp4a-latm" },
{ ".m4p", "audio/mp4a-latm" },
{ ".m4u", "video/vnd.mpegurl" },
{ ".m4v", "video/x-m4v" },
{ ".ma1", "audio/ma1" },
{ ".ma2", "audio/ma2" },
{ ".ma3", "audio/ma3" },
{ ".ma5", "audio/ma5" },
{ ".man", "application/x-troff-man" },
{ ".map", "magnus-internal/imagemap" },
{ ".mbd", "application/mbedlet" },
{ ".mct", "application/x-mascot" },
{ ".mdb", "application/x-msaccess" },
{ ".mdz", "audio/x-mod" },
{ ".me", "application/x-troff-me" },
{ ".mel", "text/x-vmel" },
{ ".mht", "message/rfc822" },
{ ".mhtml", "message/rfc822" },
{ ".mi", "application/x-mif" },
{ ".mid", "audio/mid" },
{ ".midi", "audio/midi" },
{ ".mif", "application/x-mif" },
{ ".mil", "image/x-cals" },
{ ".mio", "audio/x-mio" },
{ ".mmf", "application/x-skt-lbs" },
{ ".mng", "video/x-mng" },
{ ".mny", "application/x-msmoney" },
{ ".moc", "application/x-mocha" },
{ ".mocha", "application/x-mocha" },
{ ".mod", "audio/x-mod" },
{ ".mof", "application/x-yumekara" },
{ ".mol", "chemical/x-mdl-molfile" },
{ ".mop", "chemical/x-mopac-input" },
{ ".mov", "video/quicktime" },
{ ".movie", "video/x-sgi-movie" },
{ ".mp2", "video/mpeg" },
{ ".mp3", "audio/mpeg" },
{ ".mp4", "video/mp4" },
{ ".mpa", "video/mpeg" },
{ ".mpc", "application/vnd.mpohun.certificate" },
{ ".mpe", "video/mpeg" },
{ ".mpeg", "video/mpeg" },
{ ".mpg", "video/mpeg" },
{ ".mpg4", "video/mp4" },
{ ".mpga", "audio/mpeg" },
{ ".mpn", "application/vnd.mophun.application" },
{ ".mpp", "application/vnd.ms-project" },
{ ".mps", "application/x-mapserver" },
{ ".mpv2", "video/mpeg" },
{ ".mrl", "text/x-mrml" },
{ ".mrm", "application/x-mrm" },
{ ".ms", "application/x-troff-ms" },
{ ".msg", "application/vnd.ms-outlook" },
{ ".mts", "application/metastream" },
{ ".mtx", "application/metastream" },
{ ".mtz", "application/metastream" },
{ ".mvb", "application/x-msmediaview" },
{ ".mzv", "application/metastream" },
{ ".nar", "application/zip" },
{ ".nbmp", "image/nbmp" },
{ ".nc", "application/x-netcdf" },
{ ".ndb", "x-lml/x-ndb" },
{ ".ndwn", "application/ndwn" },
{ ".nif", "application/x-nif" },
{ ".nmz", "application/x-scream" },
{ ".nokia-op-logo", "image/vnd.nok-oplogo-color" },
{ ".npx", "application/x-netfpx" },
{ ".nsnd", "audio/nsnd" },
{ ".nva", "application/x-neva1" },
{ ".nws", "message/rfc822" },
{ ".oda", "application/oda" },
{ ".ogg", "audio/ogg" },
{ ".oom", "application/x-AtlasMate-Plugin" },
{ ".p10", "application/pkcs10" },
{ ".p12", "application/x-pkcs12" },
{ ".p7b", "application/x-pkcs7-certificates" },
{ ".p7c", "application/x-pkcs7-mime" },
{ ".p7m", "application/x-pkcs7-mime" },
{ ".p7r", "application/x-pkcs7-certreqresp" },
{ ".p7s", "application/x-pkcs7-signature" },
{ ".pac", "audio/x-pac" },
{ ".pae", "audio/x-epac" },
{ ".pan", "application/x-pan" },
{ ".pbm", "image/x-portable-bitmap" },
{ ".pcx", "image/x-pcx" },
{ ".pda", "image/x-pda" },
{ ".pdb", "chemical/x-pdb" },
{ ".pdf", "application/pdf" },
{ ".pfr", "application/font-tdpfr" },
{ ".pfx", "application/x-pkcs12" },
{ ".pgm", "image/x-portable-graymap" },
{ ".pict", "image/x-pict" },
{ ".pko", "application/ynd.ms-pkipko" },
{ ".pm", "application/x-perl" },
{ ".pma", "application/x-perfmon" },
{ ".pmc", "application/x-perfmon" },
{ ".pmd", "application/x-pmd" },
{ ".pml", "application/x-perfmon" },
{ ".pmr", "application/x-perfmon" },
{ ".pmw", "application/x-perfmon" },
{ ".png", "image/png" },
{ ".pnm", "image/x-portable-anymap" },
{ ".pnz", "image/png" },
{ ".pot,", "application/vnd.ms-powerpoint" },
{ ".ppm", "image/x-portable-pixmap" },
{ ".pps", "application/vnd.ms-powerpoint" },
{ ".ppt", "application/vnd.ms-powerpoint" },
{ ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
{ ".pqf", "application/x-cprplayer" },
{ ".pqi", "application/cprplayer" },
{ ".prc", "application/x-prc" },
{ ".prf", "application/pics-rules" },
{ ".prop", "text/plain" },
{ ".proxy", "application/x-ns-proxy-autoconfig" },
{ ".ps", "application/postscript" },
{ ".ptlk", "application/listenup" },
{ ".pub", "application/x-mspublisher" },
{ ".pvx", "video/x-pv-pvx" },
{ ".qcp", "audio/vnd.qcelp" },
{ ".qt", "video/quicktime" },
{ ".qti", "image/x-quicktime" },
{ ".qtif", "image/x-quicktime" },
{ ".r3t", "text/vnd.rn-realtext3d" },
{ ".ra", "audio/x-pn-realaudio" },
{ ".ram", "audio/x-pn-realaudio" },
{ ".rar", "application/octet-stream" },
{ ".ras", "image/x-cmu-raster" },
{ ".rc", "text/plain" },
{ ".rdf", "application/rdf+xml" },
{ ".rf", "image/vnd.rn-realflash" },
{ ".rgb", "image/x-rgb" },
{ ".rlf", "application/x-richlink" },
{ ".rm", "audio/x-pn-realaudio" },
{ ".rmf", "audio/x-rmf" },
{ ".rmi", "audio/mid" },
{ ".rmm", "audio/x-pn-realaudio" },
{ ".rmvb", "audio/x-pn-realaudio" },
{ ".rnx", "application/vnd.rn-realplayer" },
{ ".roff", "application/x-troff" },
{ ".rp", "image/vnd.rn-realpix" },
{ ".rpm", "audio/x-pn-realaudio-plugin" },
{ ".rt", "text/vnd.rn-realtext" },
{ ".rte", "x-lml/x-gps" },
{ ".rtf", "application/rtf" },
{ ".rtg", "application/metastream" },
{ ".rtx", "text/richtext" },
{ ".rv", "video/vnd.rn-realvideo" },
{ ".rwc", "application/x-rogerwilco" },
{ ".s3m", "audio/x-mod" },
{ ".s3z", "audio/x-mod" },
{ ".sca", "application/x-supercard" },
{ ".scd", "application/x-msschedule" },
{ ".sct", "text/scriptlet" },
{ ".sdf", "application/e-score" },
{ ".sea", "application/x-stuffit" },
{ ".setpay", "application/set-payment-initiation" },
{ ".setreg", "application/set-registration-initiation" },
{ ".sgm", "text/x-sgml" },
{ ".sgml", "text/x-sgml" },
{ ".sh", "application/x-sh" },
{ ".shar", "application/x-shar" },
{ ".shtml", "magnus-internal/parsed-html" },
{ ".shw", "application/presentations" },
{ ".si6", "image/si6" },
{ ".si7", "image/vnd.stiwap.sis" },
{ ".si9", "image/vnd.lgtwap.sis" },
{ ".sis", "application/vnd.symbian.install" },
{ ".sit", "application/x-stuffit" },
{ ".skd", "application/x-Koan" },
{ ".skm", "application/x-Koan" },
{ ".skp", "application/x-Koan" },
{ ".skt", "application/x-Koan" },
{ ".slc", "application/x-salsa" },
{ ".smd", "audio/x-smd" },
{ ".smi", "application/smil" },
{ ".smil", "application/smil" },
{ ".smp", "application/studiom" },
{ ".smz", "audio/x-smd" },
{ ".snd", "audio/basic" },
{ ".spc", "application/x-pkcs7-certificates" },
{ ".spl", "application/futuresplash" },
{ ".spr", "application/x-sprite" },
{ ".sprite", "application/x-sprite" },
{ ".sdp", "application/sdp" },
{ ".spt", "application/x-spt" },
{ ".src", "application/x-wais-source" },
{ ".sst", "application/vnd.ms-pkicertstore" },
{ ".stk", "application/hyperstudio" },
{ ".stl", "application/vnd.ms-pkistl" },
{ ".stm", "text/html" },
{ ".svg", "image/svg+xml" },
{ ".sv4cpio", "application/x-sv4cpio" },
{ ".sv4crc", "application/x-sv4crc" },
{ ".svf", "image/vnd" },
{ ".svg", "image/svg+xml" },
{ ".svh", "image/svh" },
{ ".svr", "x-world/x-svr" },
{ ".swf", "application/x-shockwave-flash" },
{ ".swfl", "application/x-shockwave-flash" },
{ ".t", "application/x-troff" },
{ ".tad", "application/octet-stream" },
{ ".talk", "text/x-speech" },
{ ".tar", "application/x-tar" },
{ ".taz", "application/x-tar" },
{ ".tbp", "application/x-timbuktu" },
{ ".tbt", "application/x-timbuktu" },
{ ".tcl", "application/x-tcl" },
{ ".tex", "application/x-tex" },
{ ".texi", "application/x-texinfo" },
{ ".texinfo", "application/x-texinfo" },
{ ".tgz", "application/x-compressed" },
{ ".thm", "application/vnd.eri.thm" },
{ ".tif", "image/tiff" },
{ ".tiff", "image/tiff" },
{ ".tki", "application/x-tkined" },
{ ".tkined", "application/x-tkined" },
{ ".toc", "application/toc" },
{ ".toy", "image/toy" },
{ ".tr", "application/x-troff" },
{ ".trk", "x-lml/x-gps" },
{ ".trm", "application/x-msterminal" },
{ ".tsi", "audio/tsplayer" },
{ ".tsp", "application/dsptype" },
{ ".tsv", "text/tab-separated-values" },
{ ".ttf", "application/octet-stream" },
{ ".ttz", "application/t-time" },
{ ".txt", "text/plain" },
{ ".uls", "text/iuls" },
{ ".ult", "audio/x-mod" },
{ ".ustar", "application/x-ustar" },
{ ".uu", "application/x-uuencode" },
{ ".uue", "application/x-uuencode" },
{ ".vcd", "application/x-cdlink" },
{ ".vcf", "text/x-vcard" },
{ ".vdo", "video/vdo" },
{ ".vib", "audio/vib" },
{ ".viv", "video/vivo" },
{ ".vivo", "video/vivo" },
{ ".vmd", "application/vocaltec-media-desc" },
{ ".vmf", "application/vocaltec-media-file" },
{ ".vmi", "application/x-dreamcast-vms-info" },
{ ".vms", "application/x-dreamcast-vms" },
{ ".vox", "audio/voxware" },
{ ".vqe", "audio/x-twinvq-plugin" },
{ ".vqf", "audio/x-twinvq" },
{ ".vql", "audio/x-twinvq" },
{ ".vre", "x-world/x-vream" },
{ ".vrml", "x-world/x-vrml" },
{ ".vrt", "x-world/x-vrt" },
{ ".vrw", "x-world/x-vream" },
{ ".vts", "workbook/formulaone" },
{ ".wav", "audio/x-wav" },
{ ".wax", "audio/x-ms-wax" },
{ ".wbmp", "image/vnd.wap.wbmp" },
{ ".wcm", "application/vnd.ms-works" },
{ ".wdb", "application/vnd.ms-works" },
{ ".web", "application/vnd.xara" },
{ ".wi", "image/wavelet" },
{ ".wis", "application/x-InstallShield" },
{ ".wks", "application/vnd.ms-works" },
{ ".wm", "video/x-ms-wm" },
{ ".wma", "audio/x-ms-wma" },
{ ".wmd", "application/x-ms-wmd" },
{ ".wmf", "application/x-msmetafile" },
{ ".wml", "text/vnd.wap.wml" },
{ ".wmlc", "application/vnd.wap.wmlc" },
{ ".wmls", "text/vnd.wap.wmlscript" },
{ ".wmlsc", "application/vnd.wap.wmlscriptc" },
{ ".wmlscript", "text/vnd.wap.wmlscript" },
{ ".wmv", "audio/x-ms-wmv" },
{ ".wmx", "video/x-ms-wmx" },
{ ".wmz", "application/x-ms-wmz" },
{ ".wpng", "image/x-up-wpng" },
{ ".wps", "application/vnd.ms-works" },
{ ".wpt", "x-lml/x-gps" },
{ ".wri", "application/x-mswrite" },
{ ".wrl", "x-world/x-vrml" },
{ ".wrz", "x-world/x-vrml" },
{ ".ws", "text/vnd.wap.wmlscript" },
{ ".wsc", "application/vnd.wap.wmlscriptc" },
{ ".wv", "video/wavelet" },
{ ".wvx", "video/x-ms-wvx" },
{ ".wxl", "application/x-wxl" },
{ ".x-gzip", "application/x-gzip" },
{ ".xaf", "x-world/x-vrml" },
{ ".xar", "application/vnd.xara" },
{ ".xbm", "image/x-xbitmap" },
{ ".xdm", "application/x-xdma" },
{ ".xdma", "application/x-xdma" },
{ ".xdw", "application/vnd.fujixerox.docuworks" },
{ ".xht", "application/xhtml+xml" },
{ ".xhtm", "application/xhtml+xml" },
{ ".xhtml", "application/xhtml+xml" },
{ ".xla", "application/vnd.ms-excel" },
{ ".xlc", "application/vnd.ms-excel" },
{ ".xll", "application/x-excel" },
{ ".xlm", "application/vnd.ms-excel" },
{ ".xls", "application/vnd.ms-excel" },
{ ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
{ ".xlt", "application/vnd.ms-excel" },
{ ".xlw", "application/vnd.ms-excel" },
{ ".xm", "audio/x-mod" },
{ ".xml", "text/plain" },
{ ".xml", "application/xml" },
{ ".xmz", "audio/x-mod" },
{ ".xof", "x-world/x-vrml" },
{ ".xpi", "application/x-xpinstall" },
{ ".xpm", "image/x-xpixmap" },
{ ".xsit", "text/xml" },
{ ".xsl", "text/xml" },
{ ".xul", "text/xul" },
{ ".xwd", "image/x-xwindowdump" },
{ ".xyz", "chemical/x-pdb" },
{ ".yz1", "application/x-yz1" },
{ ".z", "application/x-compress" },
{ ".zac", "application/x-zaurus-zac" },
{ ".zip", "application/zip" },
{ ".json", "application/json" }
};
//根据文件后缀名获得对应的MIME类型
public static String getMIMEType(File file) {
String type = "*/*";
String fName = file.getName();
// 获取后缀名前的分隔符"."在fName中的位置。
int dotIndex = fName.lastIndexOf(".");
if (dotIndex < 0) {
return type;
}
/* 获取文件的后缀名 */
String end = fName.substring(dotIndex, fName.length()).toLowerCase();
if (end == "")
return type;
// 在MIME和文件类型的匹配表中找到对应的MIME类型。
for (int i = 0; i < MIME_MapTable.length; i++) {
if (end.equals(MIME_MapTable[i][0]))
= MIME_MapTable[i][1];
type }
return type;
}
= new Intent();
Intent intent //方法1,指定context、class
.setComponent(new ComponentName(getApplicationContext(), MyActivity.class));
intent
//方法2,指定context、action
.setComponent(new ComponentName(getApplicationContext(), "com.myapp.intent.MyActivity"));
intent
//方法3,用于匹配其他包的目标,指定包名、全类名
.setComponent(new ComponentName("com.myapp.other", "com.myapp.other.OtherActivity")); intent
FLAG可以指定Activity的启动方式(参考前面的”Activity的四种加载模式”)
在Service等非Activity中启动Activity的时候,必须指定FLAG_ACTIVITY_NEW_TASK
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent
显式:只要指定了Component、class、packageName的其中一个,都是显式的
隐式:不直接指定启动哪个Activity(或者其他Context),而是通过Action、Data、Category来筛选除合适的Activity(或者其他Context)
Android5.0以后,禁止使用隐式Intent来启动Service,否则会报错(service intent must be explicit)
使用context.getSystemService
可以获取各种系统管理工具,如
= (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); Vibrator mVibrator
服务有:
通过ContextCompat.checkSelfPermission
判断是否拥有授权,如果没有,就通过ActivityCompat.requestPermissions
尝试获取权限,然后在onRequestPermissionsResult
回调中检测用户是否授权,并执行对应的操作
//需要授权的地方
@Override
public void onClick(View v) {
//Android 6.0 以上才需要动态获取权限
if (Build.VERSION.SDK_INT >= 23) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
//未授权,尝试申请权限
this.requestPermissions(new String[]{Manifest.permission.CALL_PHONE}, 1001);//系统会弹框让用户授权,然后调用onRequestPermissionsResult
} else{
//已授权,直接执行
makeCall();
}
} else {
//不需要动态获取权限,直接执行
makeCall();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 1001:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
//同意权限申请
makeCall();
} else if (requestPermissionAgain()) {
//再次尝试申请权限成功
makeCall();
}
break;
default:
break;
}
}
//再次尝试申请权限,带自定义的提示框
private boolean requestPermissionAgain(){
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
//未授权
//检查是否需要弹出提示框
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permissionList.get(i))) {
//用户上次点击了此次拒绝,再次弹框询问用户是否授权(app内弹框)
new AlertDialog.Builder(this)
.setTitle("未授权")
.setMessage("需要电话权限才能开始打电话")
.setNegativeButton("取消", null)
.setPositiveButton("立即授权", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, 1001);//在app内弹框
ActivityCompat}
})
.setCancelable(false)
.show();
} else {
//用户第一次使用 或者 用户拒绝且勾选不再提示(弹框提示用户说如果不给权限就不能用,需要在设置中打开权限,因为这时再调ActivityCompat.requestPermissions也不会出现授权框了)
//由于无法区分是用户第一次使用还是用户拒绝且勾选不再提示,所以需要在调用本方法前先申请一次权限,如果拒绝了再掉本方法,这样可以去掉第一次使用的情况
new AlertDialog.Builder(this)
.setTitle("未授权")
.setMessage("需要电话权限才能开始打电话")
.setNegativeButton("取消", null)
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
= new Intent();
Intent intent .setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent= Uri.fromParts("package", getPackageName(), null);
Uri uri .setData(uri);
intent.startActivity(intent);//去设置中打开权限
context}
})
.setCancelable(false)
.show();
}
return false;
} else {
return true;
}
}
要注意的是,Activity和Fragment授权的函数是不同的,如果Fragment使用了Activity的函数进行授权,是不会出现授权提示框的,反之亦然
public static void getPermissionInFragment(Fragment fragment, String permission, int requestCode) {
if (ContextCompat.checkSelfPermission(fragment.getContext(), permission) != PackageManager.PERMISSION_GRANTED) {
.requestPermissions(new String[]{permission}, requestCode);
fragment}
}
public static void getPermissionInActivity(Activity activity, String permission, int requestCode) {
if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
.requestPermissions(new String[]{permission}, requestCode);
activity}
}
= (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationManager notificationManager
//创建点击通知时发送的intent
= new Intent(context, MainActivity.class);
Intent resultIntent = TaskStackBuilder.create(context);//点击通知打开Activity,在Activity中按返回键时回到MainActivity,而不是回到主屏幕
TaskStackBuilder stackBuilder .addParentStack(MainActivity.class);//添加Activity到返回栈
stackBuilder.addNextIntent(resultIntent);//添加Intent到栈顶
stackBuilder= stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent resultPendingIntent
//创建删除通知时发送的intent
= new Intent(context, NotificationService.class);
Intent deleteIntent .setAction("xxx");//删除时往Service中发消息
deleteIntent= PendingIntent.getService(context, 0, deleteIntent, 0);
PendingIntent deletePendingIntent
//创建通知
Notification.Builder notificationBuilder = new Notification.Builder(context, NotificationChannels.LOW)
.setContentTitle("通知")//设置通知标题
.setContentText("内容")//设置通知内容
.setSmallIcon(R.mipmap.ic_notification)//设置通知栏处的小图标(需要背景透明的png图片)
.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_notifiation_big))//设置展开通知栏后显示的大图标
.setColor(Color.parseColor("#8161F6"))//展开通知栏后会在大图标的右下角显示小图标(Android 5.x和6.x中),可以通过setColor设置小图标的背景色
.setAutoCancel(true)//设置点击通知后自动删除通知
//.setOngoing(true)//设置通知不可删除
.setWhen(System.currentTimeMillis())//设置通知时间
.setShowWhen(true)//设置显示通知时间
.setContentIntent(resultPendingIntent)//设置点击通知时的响应事件
.setDeleteIntent(deletePendingIntent);//设置删除通知时的响应事件
/*要使用带可点击控件的通知(比如快速回复、确定取消、音乐播放暂停按钮等),可以使用Notification.Action.Builder创建,再通过builder.setActions设置
比如带两个按钮的通知:
Notification.Action yesAction = new Notification.Action.Builder(
Icon.createWithResource("", R.mipmap.ic_yes),
"YES",
yesPendingIntent)
.build();
Notification.Action noAction = new Notification.Action.Builder(
Icon.createWithResource("", R.mipmap.ic_no),
"NO",
noPendingIntent)
.build();
notificationBuilder.setActions(yesAction, noAction);
*/
/*要使用其他样式的通知,可以使用Notification.xxxStyle内部类创建,再通过builder.setStyle设置
比如大图效果通知
Notification.BigPictureStyle bigPictureStyle = new Notification.BigPictureStyle()
.setBigContentTitle("这是一个大图效果的通知")
.setSummaryText("通知内容,图片的文字描述")
.bigPicture(BitmapFactory.decodeResource(context.getResources(), R.mipmap.big_style_picture));
notificationBuilder.setStyle(bigPictureStyle);
*/
/*自定义通知样式
RemoteViews customView = new RemoteViews(context.getPackageName(),R.layout.custom_view_layout);
RemoteViews customBigView = new RemoteViews(context.getPackageName(), R.layout.custom_big_view_layout);
customBigView.setImageViewBitmap(R.id.iv_content, BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_icon));
customBigView.setTextViewText(R.id.tv_title, "标题");
customBigView.setTextViewText(R.id.tv_summery, "内容");
customBigView.setOnClickPendingIntent(R.id.iv_content, pendingIntent);
notificationBuilder.setCustomContentView(customView);//设置自定义小视图
notificationBuilder.setCustomBigContentView(customBigView);//设置自定义大视图
*/
//在Android 8.0 及以上的版本,需要设置NotificationChannel才能正常使用通知
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
= new NotificationChannel(channelID, channelName, NotificationManager.IMPORTANCE_HIGH);//不同的重要等级有不同的显示方式
NotificationChannel channel .setDescription(description);
channel.enableLights(true);//是否在桌面图标右上角展示小红点
channel.setLightColor(Color.RED);//小红点颜色
channel.enableVibration(true);//如果不设置声音和振动,会按照重要等级按系统默认的方式来播放对应的通知效果
channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
channel/*或者禁用声音和振动
channel.setSound(null,null);
channel.setVibrationPattern(null);*/
.setShowBadge(false);//是否在长按桌面图标时显示此channel的通知
channel
.createNotificationChannel(channel);//创建Channel
notificationManager
.setChannelId(channelID);//设置channelID
notificationBuilder}
//发送通知
.notify(notificationID, notificationBuilder.build()); notificationManager
Android Studio下的配置
1、新建一个项目,在File->Project Structure中指定NDK的路径,路径不能含有空格,而且最好不要有中文
2、建一个JAVA类,将要调用的C中的函数声明为native,同时通过静态代码块加载so文件,然后Rebuild
Project,就会在app\build\intermediates
目录下新生成classes文件夹
package com.myapp.jnidemo;
public class JniUtils {
static{
System.loadLibrary("MyJniLib");
}
//JAVA调C
public native String javaCallC(String str);
//JAVA调C,然后C回调JAVA中的cCallJava函数
public native void cCallJavaCallback();
//C调JAVA的函数
public int cCallJava(int i, int j){
.i("TAG", "C调用了JAVA");
Logreturn (i + j);
}
3、生成对应的头文件:
打开AS Terminal(或者cmd也可以),在AS
Terminal中切换目录至app/build/intermediates/classes/debug
,输入javah -jni com.myapp.jnidemo.JniUtils
(javah -jni 全类名
),这时就会在debug文件夹中生成com_myapp_jnidemo_JniUtils.h
(可能会有编译错误,能生成头文件就行),这时再把这个头文件剪切到src/main/jni
下(jni文件夹需手动创建)
一般情况下在app/src/main
使用javah -jni 全类名
也可以,但可能会出现无法生成头文件的情况,所以最好还是在app/build/intermediates/classes/debug
下执行javah
在JDK10中已经把javah
命令删除,使用javac -h 生成的头文件目录 java文件
(如javac -h jnidir Test.java
)替代
4、写对应的C文件
再在jni文件夹创建一个JniTest.c文件(名字可随便取),JAVA对应C的函数名可以在头文件中查看,其固定格式为Java_包名_类名_方法名
(把.
改成_
),前两个参数也固定为JNIEnv *env
和jobject obj
,JAVA的基本数据类型对应的C类型及常用方法已经在jni.h中定义好,可以在jni.h中查看(在NDK目录搜jni.h),对基本数据类型和反射JAVA类的操作几乎都是由JNIEnv完成
C调JAVA函数时用到的方法签名可以在app/build/intermediates/classes/debug
中使用javap -s 全类名
得到类中所有方法的签名
JniTest.c:
#include "com_myapp_jnidemo_JniUtils.h"
//JNIEXPORT和JNICALL可以删掉,不影响运行
//jAVA要调的C函数,方法名遵循"Java_包名_类名_方法名"的格式
(JNIEnv *env, jobject obj, jstring str){
JNIEXPORT jstring JNICALL Java_com_myapp_jnidemo_JniUtils_javaCallCreturn (*env)->NewStringUTF(env,"I am from C!");
}
//C调JAVA的函数,因为要用到JNIEnv,所以一般用于JAVA调C,然后C回调JAVA
//jobject就是调用该函数的类对象,这里是JniUtils
void JNICALL Java_com_myapp_jnidemo_JniUtils_cCallJavaCallback(JNIEnv *env, jobject obj){
JNIEXPORT //1.得到字节码,第二个参数为"全类名",把"."改成"/"
= (*env)->FindClass(env, "com/myapp/jnidemo/JniUtils");
jclass jclazz //2.得到方法ID,第四个参数为方法签名,可以在debug目录下使用"javap -s 全类名",来得到类的所有方法签名(因为函数可以重裁,只有方法名还不知道要调哪个方法,需指定方法签名)
= (*env)->GetMethodID(env, jclazz, "cCallJava", "(II)I");
jmethodID methodID //3.实例化对象
= (*env)->AllocObject(env, jclazz);
jobject jobj //4.调用方法
= (*env)->CallIntMethod(env, jobj, methodID, 10, 20);
jint result }
5、配置编译参数
配置App目录下的build.gradle,添加ndk节点:
{
buildTypes {
release false
minifyEnabled getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles {
ndk"MyJniLib"//生成的so名字
moduleName "armeabi", "armeabi-v7a", "x86"//输出指定三种abi体系结构下的so库,让代码可以在这三种环境下运行
abiFilters }
}
{
debug{
ndk"MyJniLib"
moduleName "armeabi", "armeabi-v7a", "x86"
abiFilters }
}
}
配置gradle.properties,让Android Studio兼容旧版的NDK
android.useDeprecatedNdk=true
6、至此JNI已配置完成,可以在JAVA中使用NDK了
= new JniUtils();
JniUtils jniUtils String str = jniUtils.javaCallC(“java”);
.cCallJavaCallback();
jniUtils.i("TAG",str) Log
参考
腾讯项目 Tinker
阿里项目 AndFix
https://material.io/
参考文章