Activity的四种加载模式(launchMode)

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可以叠加使用

Intent intent = new Intent(this, MyActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

常用的FLAG有:

Activity的生命周期

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

使用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

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

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>

OptionsMenu

在Activity中点击菜单键触发,用法:在Activity中重写onCreateOptionsMenu、onOptionsItemSelected

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuItem item = menu.add(1, 100, 1, "菜单一");
    item.setTitle("aaa");
    item.setIcon(R.drawable.ic_launcher);
    menu.add(1, 101, 2, "菜单二");
    menu.add(1, 102, 1, "菜单三");
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch(item.getItemId()) {
    case 100:
        Toast.makeText(MainActivity.this, "点击了菜单一", Toast.LENGTH_SHORT).show();
        break;
    case 101:
        //xxx
        break;
    case 102:
        //xxx
        break;
    }
    return super.onOptionsItemSelected(item);
}

ContextMenu

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++){
            list.add("file"+(i+1));
        }
        ArrayAdapter<String> adapter=new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,list);
        listView.setAdapter(adapter);
        this.registerForContextMenu(listView);//为listview注册上下文菜单
    }

    //创建菜单
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        //设置mune显示的内容
        menu.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");
    }
    //响应菜单
    @Override
    public boolean onContextItemSelected(MenuItem item) {
        switch (item.getItemId()){
            case 1:
                Toast.makeText(this, "clicked copy",Toast.LENGTH_SHORT).show();
                break;
            case 2:
                //xxx
                break;
            case 3:
                //xxx
                break;
            case 4:
                //xxx
                break;
        }
        return super.onContextItemSelected(item);
    }

AlertDialog

普通的确认对话框:

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("请做出选择").setIcon(R.drawable.ic_launcher)
        .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
                    }
                });
builder.create().show();

选项列表对话框:

final String[] items = new String[] { "北京", "上海", "广州", "深圳" };
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.drawable.ic_launcher).setTitle("标题")
        .setItems(items, new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                // TODO
            }
        });
builder.create().show();

单选列表对话框:

final String[] items = new String[] { "北京", "上海", "广州", "深圳" };
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.drawable.ic_launcher).setTitle("标题")
        .setSingleChoiceItems(items, 0, new OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                // TODO
            }
        });
builder.create().show();

多选列表对话框:

final String[] items = new String[] { "北京", "上海", "广州", "深圳" };
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.drawable.ic_launcher)
        .setTitle("标题")
        .setMultiChoiceItems(items,
                new boolean[] { true, true, false, false, false },
                new OnMultiChoiceClickListener() {
                    public void onClick(DialogInterface dialog,int which, boolean isChecked) {
                        if (isChecked) {
                            Toast.makeText(MainActivity.this,items[which], 0).show();
                        }
                    }
                });
builder.create().show();

自定义列表项:

final String[] items = new String[] { "北京", "上海", "广州", "深圳" };
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("标题")
        .setIcon(R.drawable.ic_launcher)
        .setAdapter(
                new ArrayAdapter<String>(MainActivity.this,
                        R.layout.item, R.id.tv, items),
                new OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        // TODO
                        Toast.makeText(MainActivity.this, items[which], 0).show();
                    }
                });
builder.create().show();

自定义View列表:

AlertDialog.Builder builder = new AlertDialog.Builder(this);
View 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);

builder.setTitle("登录").setIcon(R.drawable.ic_launcher)
        .setView(view2);
final AlertDialog alertDialog = builder.create();
button.setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {
        String uname = username.getText().toString().trim();
        String psd = password.getText().toString().trim();
        if (uname.equals("username") && psd.equals("password")) {
            Toast.makeText(MainActivity.this, "登录成功", 0).show();
        }
        Toast.makeText(MainActivity.this, "登录失败", 0).show();
        alertDialog.dismiss();// 对话框消失
    }
});
alertDialog.show();

日期时间Dialog

//获取当前年月日、时间
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();

ListView

常用属性有: - 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>();
    map.put("ItemImage", R.drawable.icon);//加入图片
    map.put("ItemTitle", "第"+i+"行");
    map.put("ItemText", "这是第"+i+"行");
    listItem.add(map);
}

SimpleAdapter mSimpleAdapter = new SimpleAdapter( MainActicity.this, listItem, R.layout.item, new String[]{"ItemImage","ItemTitle", "ItemText"}, new int[] {R.id.ItemImage,R.id.ItemTitle,R.id.ItemText} );

listview.setChoiceMode(ListView.CHOICE_MODE_SINGLE);//设置选择模式为单选
listview.setAdapter(mSimpleAdapter);
lv.setOnItemClickListener(new OnItemClickListener(){
    public 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){
            convertView = inflater.inflate(R.layout.layout_student_item,null);
            ViewHolder viewHolder = new 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);
            convertView.setTag(viewHolder);
            view = convertView;
        }else{
            view = convertView;
        }
        Student student=getItem(position);
        ViewHolder mViewHolder = view.getTag();
        mViewHolder.image_photo.setImageResource(student.getPhoto());
        mViewHolder.tv_name.setText(student.getName());
        mViewHolder.tv_age.setText(String.valueOf(student.getAge()));
        return view;
    }
class ViewHolder{
    ImageView image_photo;
    TextView tv_name;
    TextView tv_age;
}
}

更新ListView的数据:

//改变数据集(stuList),然后调
adapter.notifyDataSetChanged();

ViewPager

使用

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());
       list.add(id);
    }

    ViewPager viewpager = (ViewPager) findViewById(R.id.viewpager);
    //设置PagerAdapter,如果与Fragment一起使用,使用的Adapter就是FragmentPagerAdapter或FragmentStatePagerAdapter
    viewpager.setAdapter(new MyPagerAdapter(this,list));
    //设置翻页动画
    viewpager.setPageTransformer(false,new ViewPager.PageTransformer(){
        public void transformPage(View view, float position) {
            //根据position对view进行动画设置
        }
    });
    //设置翻页监听
    viewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener(){
        public 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) {
        mContext = context;
        mData = list;
    }

    @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);
        img.setImageResource(list.get(position));
        container.addView(view);
        return view;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View)object);
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }
}

原理

ViewPager其实是通过view.scrollToview.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同理
            lastX = event.getX();
            lastY = event.getY();
            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);
            lastX = event.getX();
            lastY = event.getY();
            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 mScroller;
protected void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mScroller = new Scroller(MyActivity.this);
    button.setOnClickListener(new OnClickListener(){
        public void onClick(View view) {
            //获取屏幕宽度
            DisplayMetrics metric = new DisplayMetrics();
            context.getWindowManager().getDefaultDisplay().getRealMetrics(metric);
            int width = metric.widthPixels;
            //startScroll(int startX,int startY,int dx,int dy,int duration);
            //从(0, 0)开始,向右滑动一个屏幕,总共500毫秒完成
            mScroller.startScroll(0,0, -width, 0, 500);
            invalidate();
        }
    });
}

//调用invalidate或postInvalidate时,除了调用onDraw外,还会调computeScroll
public void computeScroll(){
    if (mScroller.computeScrollOffset()) {
        //还没有完成滑动,继续滑动
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
    super.computeScroll();
}

手势操作还可以通过GestureDetectorViewDragHelper实现

private GestureDetector mDetector;

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //OnGestureListener也可以用SimpleOnGestureListener,其实就是一个把抽象方法改成空方法的OnGestureListener,不用写那么多个用不到的函数而已
    mDetector = new GestureDetector(MyActivity.this,new GestureDetector.OnGestureListener(){
        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时已赋值
Scroller mScroller;//系统提供的计算回弹坐标的类,在onCreate时已赋值
float 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:
            downX = event.getX();
            downY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            float endX = event.getX();
            float endY = event.getY();
            floast DX = endX - downX;
            floast DY = endY - downY;
            if(Math.abs(DX) > 8){//水平滑动超过8px,则为水平滑动
                intercept = true;
            }
            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:
            lastX = event.getX();
            lastY = event.getY();
            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){
                toScrollX = 0;
            }else if(toScrollX > menuwidth){
                toScrollX = toScrollX;
            }
            scrollTo(toScrollX, getScrollY());
            lastX = event.getX();
            lastY = event.getY();
            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(){
    mScroller.startScroll(getScrollX(), getScrollY(), menuwidth - getScrollX(), getScrollY())
    invalidate();
}

//回弹菜单(水平方向回弹,竖直方向不变)
public void closeMenu(){
    mScroller.startScroll(getScrollX(), getScrollY(), - getScrollX(), getScrollY())
    invalidate();
}

//由于用到了Scroller,需重写此方法
public void computeScroll(){
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
    super.computeScroll();
}

要自动滑动ViewPager,可以用handler发延时消息,这里只提供思路,不再列出具体实现

TabLayout+ViewPager

使用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

fragmentList = new ArrayList<>();
list_Title = new ArrayList<>();
fragmentList.add(new MyFragment());
fragmentList.add(new MyFragment());
list_Title.add("one");
list_Title.add("two");
viewpager.setAdapter(new MyPagerAdapter(getSupportFragmentManager(), MyActivity.this, fragmentList, list_Title));

tablayout.setupWithViewPager(viewpager);//此方法就是让tablayout和ViewPager联动
tablayout.setTabMode(TabLayout.MODE_SCROLLABLE);//如果标签很多,需要设置可滑动

RecyclerView

使用前需在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中配置

mRecyclerView = findViewById(R.id.recyclerView);
//设置LayoutManager,可以通过LayoutManager设置显示为线性、网格(GridLayoutManager)、瀑布流(StaggeredGridLayoutManager,item高度不一致时使用)等
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
//设置添加或删除item时的动画,这里使用默认动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
//添加分割线,添加多个时叠加显示
//ItemDecoration内置里一共有三个类继承该类,分别是 DividerItemDecoration(普通分割线,可以实现section(联系人列表中的按首字母拼音分组)、StickyHeader等效果),ItemTouchHelper(可以实现拖拽和移动,侧滑删除等效果),FastScroller(包权限,不开放给外部使用)
recyclerView.addItemDecoration(new DividerItemDecoration(MainActivity.this, RecyclerView.ItemDecoration.HORIZONTAL));
//设置适配器
mAdapter = new MyRecyclerViewAdapter(list); 
mRecyclerView.setAdapter(mAdapter);

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);
        MyAdapter.ViewHolder viewHolder = new MyAdapter.ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(MyAdapter.ViewHolder holder, int position) {
        holder.mText.setText(list.get(position));
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        TextView mText;
        ViewHolder(View itemView) {
            super(itemView);
            mText = itemView.findViewById(R.id.item_tx);
        }
    }
}

FloatButton

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是桌面小部件,比如常见的天气预报、时钟等就是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);
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
        views.setTextViewText(R.id.appwidget_text, widgetText);
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    //当小部件被添加时或者每次小部件更新时都会调用一次该方法
    @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,然后通过RemoteViewssetXXX方法对AppWidget的内容进行更新,最后通过updateAppWidget执行更新

这里的my_app_widget是一个普通的布局文件,但是因为它只能通过RemoteViewssetXXX方法进行更新,所以能更新的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) {
    Intent intent = new Intent(context, MainActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,0);
    RemoteViews view = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
    view.setOnClickPendingIntent(R.drawable.ic_launcher_background, pendingIntent);
    appWidgetManager.updateAppWidget(R.layout.my_app_widget,view);
}

Shortcuts

长按应用图标显示应用快捷键菜单(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中配置activitymeta-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>

要注意的是,能配置shortcutsactivity必须是android.intent.action.MAINandroid.intent.category.LAUNCHER

静态配置的shortcuts无法设置Intent的flag,flag默认为FLAG_ACTIVITY_NEW_TASKFLAG_ACTIVITY_CLEAR_TASK,也就是如果当前Activity已经启动,通过shortcuts启动应用会把当前Activity销毁

动态配置

通过ShortcutManager配置

添加:

ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);

ShortcutInfo shortcut = new ShortcutInfo.Builder(context, "id1")
    .setShortLabel("Website")
    .setLongLabel("Open the website")
    .setIcon(Icon.createWithResource(context, R.drawable.icon_website))
    .setIntent(new Intent(Intent.ACTION_VIEW,
                   Uri.parse("https://www.mysite.example.com/")))
    .build();

shortcutManager.setDynamicShortcuts(Arrays.asList(shortcut));

更新:

List<ShortcutInfo> infoList = shortcutManager.getPinnedShortcuts();
//修改infoList
//...
shortcutManager.updateShortcuts(infoList);

删除:

ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
List<ShortcutInfo> infoList = shortcutManager.getDynamicShortcuts();
List<String> idList = new ArrayList<>();
for(ShortcutInfo s : infoList){
    idList.add(s.getId());
}
shortcutManager.disableShortcuts(idList, "已禁用");
shortcutManager.removeDynamicShortcuts(idList);

Pinned Shortcuts

给Shortcuts条目生成应用图标放到桌面,Android8.0加入的功能

ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);

if (shortcutManager.isRequestPinShortcutSupported()) {
    //对于设置为enabled的已存在的shortcut,可以直接通过id获取
    //但如果是新建一个shortcut,就要指定id、shortLabel、intent
    //这里假设id为my-shortcut的shortcut已存在且为enabled
    ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(this, "my-shortcut").build();

    //当用户尝试给shortcut创建桌面快捷方式时,我们要通过程序检测是否运行用户创建,这时就要用回调
    //对应的context需要重写createShortcutResultIntent(),并在该函数中处理回调
    Intent pinnedShortcutCallbackIntent = shortcutManager.createShortcutResultIntent(pinShortcutInfo);

    PendingIntent successCallback = PendingIntent.getActivity(this, 0, pinnedShortcutCallbackIntent, 0);

    shortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.getIntentSender());
}

对于Pinned Shortcuts,只能由用户删除,我们没有权限删除,但我们可以禁用

ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);

List<ShortcutInfo> infoList = shortcutManager.getPinnedShortcuts();
List<String> idList = new ArrayList<>();
for (ShortcutInfo info : infoList) {
    idList.add(info.getId());
}
shortcutManager.disableShortcuts(idList, "已失效");
//虽然用了remove,但实际上只是禁用了
shortcutManager.removeDynamicShortcuts(idList);

布局方式

布局的通用属性: - layout_margin:外边距 - layout_padding:内边距 - layout_width/height:宽高 - gravity:组件内容的对齐方式 - layout_gravity:自身相对于父元素的布局

LinearLayout

RelativeLayout

常用的属性有

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 父控件的中央;

FrameLayout

从屏幕左上角按照层次堆叠方式布局,后面的控件覆盖前面的控件

AbsoluteLayout

通过android:layout_xandroid:layout_y,设置坐标,屏幕左上角为坐标(0,0)

ConstraintLayout

AbsoluteLayout的替代品,对于Android Studio的拖拽控件有较好的支持,使用前需添加依赖

dependencies {
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
}

TableLayout

由多个TableRow组成,一个TableRow由若干个view组成;TableLayout常用属性有 - android:shrinkColumns 设置可收缩的列,内容过多就收缩显示到第二行;; - android:stretchColumns 设置可伸展的列,将空白区域填充满整个列; - android:collapseColumns 设置要隐藏的列;

子控件常用属性: - android:layout_column 第几列; - android:layout_span 占据列数;

GridLayout

网格布局,常用属性有 - android:rowCount 设置行数 - android:columnCount 设置列数 - android:layout_row 组件在第几行 - android:layout_column 组件在第几列 - android:layout_rowSpan 组件横跨几行 - android:layout_columnSpan 组件横跨几列 - android:layout_gravity = “fill” 组件横跨几行或列后,需要加上填充属性

DrawerLayout

抽屉布局,用于侧滑菜单(该布局在v4包:android.support.v4.widget.DrawerLayout),里面一般使用NavigationView来实现菜单项

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 mDrawerLayout;
ListView menuDrawer;//这里使用ListView显示菜单项

@Override
protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    mDrawerToggle.syncState();
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    mDrawerToggle.onConfigurationChanged(newConfig);
}

@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) {
        mDrawerLayout.closeDrawers();
        return;
    }
    super.onBackPressed();
}

SlidingPaneLayout

也是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

编辑SharePreference

/*
MODE_APPEND: 追加方式存储
MODE_PRIVATE: 私有方式存储,其他应用无法访问
MODE_WORLD_READABLE: 表示当前文件可以被其他应用读取
MODE_WORLD_WRITEABLE: 表示当前文件可以被其他应用写入
*/
SharedPreferences sharedPreferences = getSharedPreferences("filename", Context.MODE_PRIVATE);

Editor editor = sharedPreferences.edit();
editor.putString("key", "value");
editor.putInt("num", 1234);
editor.commit();//提交

sharedPreferences.getString("key", "");//第二个参数为如果key字段不存在时的默认返回值

删除SharedPreferences产生的文件

File file= new File("/data/data/"+getPackageName().toString()+"/shared_prefs","filename.xml");
if(file.exists()){
    file.delete();
}

访问其他应用的SharedPreferences(要求要访问的应用的Preference创建时指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE权限)

//创建其他应用的Context,通过该Context获取SharedPreferences
Context otherAppsContext = createPackageContext("com.myapp.app", Context.CONTEXT_IGNORE_SECURITY);

SharedPreferences sharedPreferences = otherAppsContext.getSharedPreferences("filename", Context.MODE_WORLD_READABLE);

//也可以通过直接读取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);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        Toast.makeText(mContext, "创建数据库成功!", Toast.LENGTH_SHORT).show();
    }

    //当数据库版本号不一样的时候调用,一般是app升级后使用
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);
    }
}

使用:

MyDataBaseHelper dbHelper = new MyDataBaseHelper(this,"BookStore.db",null,1);

//当磁盘没有满的时候getWritableDatabase和getReadableDatabase返回的实例都是可读可写的,没有区别;只有当磁盘满了的时候,打开数据库失败,这时getReadableDatabase会继续尝试以只读方式打开数据库,而getWritableDatabase就会直接报错
SQLiteDatabase db = dbHelper.getWritableDatabase();

ContentValues values = new ContentValues();

//插入数据
//如果insert的第三个参数为空,则表示插入一条除主键值以外其他字段为Null值的记录,为了满足SQL语法的需要, insert语句必须给定一个字段名,如:insert into person(name) values(NULL),倘若不给定字段名 , insert语句就成了这样: insert into person() values(),显然这不满足标准SQL的语法,所以第二个参数就是用来指定这种时候空字段的名称的,如果第三个参数values不为null,则可以把第二个参数设置为null
// 开始组装第一条数据
values.put("name", "The Da Vinci Code");
values.put("author", "Dan Brown");
values.put("pages", 454);
values.put("price", 16.96);
long rowid = db.insert("Book", null, values);//返回新添记录的行号,与主键id无关
values.clear();
// 开始组装第二条数据
values.put("name", "The Lost Symbol");
values.put("author", "Dan Brown");
values.put("pages", 510);
values.put("price", 19.95);
rowid = db.insert(“Book”,null,values);//返回新添记录的行号,与主键id无关
values.clear();

//修改数据
values.put("price",100);
db.update("Book",values,"name = ?",new String[]{"The Da Vinci Code"});
values.clear();

//删除数据
db.delete("Book","page > ?",new String[]{"500"});

//查询数据
//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());
}

//关闭表指针
cursor.close();

//关闭数据库
db.close()

事务处理:

MyDataBaseHelper dbHelper = new MyDataBaseHelper(this,"BookStore.db",null,1);
SQLiteDatabase db = dbHelper.getWritableDatabase();

db.beginTransaction();
try{
    db.execSQL("update Book set price=18.4 where name=?",new String[]{"The Da Vinci Code"});
    int i = 10/0;//故意设置一个异常
    db.execSQL("update Book set price=20.3 where name=?",new String[]{"The Lost Symbol"});
    db.setTransactionSuccessful();
}catch(Exception e){
    // TODO
}finally{
    db.endTransaction();
}
db.close();

网络

用到的权限

<user-permission android:name="android.permission.INTERNET"/>

HttpClient

HttpClient在Android6.0以上已被移除,但仍然可以通过Apache提供的jar包来使用,在build.gradle中配置

android {
    useLibrary 'org.apache.http.legacy'
}

Get请求:

HttpClient httpCient = new DefaultHttpClient();
HttpParams params = httpClient.getParams();
HttpConnectionParams.setConnectionTimeout(params,5000);

HttpGet httpGet = new HttpGet("http://www.baidu.com");
try{
    HttpResponse httpResponse = httpCient.execute(httpGet);
    if (httpResponse.getStatusLine().getStatusCode() == 200){
        HttpEntity entity = httpResponse.getEntity();
        String response = EntityUtils.toString(entity,"utf-8");
    }
}catch(Exception e){
    // TODO
}finally{
    httpClient.getConnectionManager().shutdown();
}

Post请求:

HttpClient httpCient = new DefaultHttpClient();
HttpParams params = httpClient.getParams();
HttpConnectionParams.setConnectionTimeout(params,5000);

List<NameValuePair> list = new ArrayList<NameValuePair>();
list.add(new BasicNameValuePair("key", "value"));
String result = "";
try{
    UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
    HttpPost httpPost = new HttpPost("http://127.0.0.1");
    httpPost.setEntity(entity);
    HttpResponse httpResponse = client.execute(httpPost);
    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) {
                    outputStream.write(data, 0, len);
                }
                result = new String(outputStream.toByteArray(), "UTF-8");
            }catch(Exception e){}
        }
    }
}catch(Exception e){
    // TODO
}finally{
    httpClient.getConnectionManager().shutdown();
}

HttpUrlConnection

GET请求:

HttpURLConnection connection = null;
BufferedReader reader = null;
String result = "";

try{
    URL url = new URL("http://127.0.0.1");
    connection = (HttpURLConnection)url.openConnection();
    connection.setRequestMethod("GET");
    connection.setConnectTimeout(8000);
    connection.setReadTimeout(8000);
    connection.setRequestProperty("Cookie", "AppName=" + URLEncoder.encode("你好", "UTF-8"));

    if(connection.getResponseCode() == 200){
        InputStream in = connection.getInputStream();
        reader = new BufferedReader(new InputStreamReader(in));
        StringBuilder response = new StringBuilder();
        String line;
        while((line = reader.readLine()) != null){
                response.append(line);
        }
        result = response.toString();
    }
}catch(Exception e){
    //TODO
}finally{
    if (reader != null){
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if (connection != null){
        connection.disconnect();
    }
}

POST请求:

HttpURLConnection connection = null;
BufferedReader reader = null;
String result = "";

try{
    URL url = new URL("http://127.0.0.1");
    connection = (HttpURLConnection)url.openConnection();
    connection.setRequestMethod("POST");
    connection.setConnectTimeout(8000);
    connection.setReadTimeout(8000);
    connection.setDoOutput(true);

    FileInputStream file = new FileInputStream("filename.png");
    OutputStream os = connection.getOutputStream();
    int count = 0;
    while((count = file.read()) != -1){
        os.write(count);
    }
    os.flush();
    os.close();



    if(connection.getResponseCode() == 200){
        InputStream in = connection.getInputStream();
        reader = new BufferedReader(new InputStreamReader(in));
        StringBuilder response = new StringBuilder();
        String line;
        while((line = reader.readLine()) != null){
                response.append(line);
        }
        result = response.toString();
    }
}catch(Exception e){
    //TODO
}finally{
    if (reader != null){
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if (connection != null){
        connection.disconnect();
    }
}

HTTPS

HTTPSHTTP over SSL/TLSHTTP是应用层协议,TCP是传输层协议,在应用层和传输层之间,增加了一个安全套接层SSL/TLSSSL/TLS层负责客户端和服务器之间的加解密算法协商、密钥交换、通信连接的建立,对HTTP而言,安全传输层是透明不可见的。

PKI、CA、RA

公钥基础设施(PKI)

假设现在A与B建立安全的连接进行通信

  1. 如果直接使用对称加密通信,那么密钥无法安全的送给B
  2. 如果直接使用非对称加密,A使用B的公钥加密,B使用私钥解密。但是因为B无法确保拿到的公钥就是A的公钥,因此也不能防止中间人攻击

为了解决上述问题,引入了一个第三方,也就是CA

CA(Certificate Authority)

CA用自己的私钥签发数字证书,数字证书中包含B的公钥。然后A可以用CARoot证书(根证书)中的公钥来解密CA签发的证书,从而拿到合法的公钥

那么又引入了一个问题,如何保证CA的公钥是合法的呢。答案就是现代主流的浏览器会内置CA的证书。

RA(Registration Authority)

RA又叫中间证书,当CARoot证书遭到破坏或者泄露时,由此CA颁发的其他证书就全部失去了安全性,所以现在主流的商业数字证书机构CA一般都是提供三级证书:Root证书签发中级RA证书,由RA证书签发用户使用的证书。这样Root证书可以离线存储来确保安全,即使RA证书出了问题,还可以用Root证书重新签署中间证书

openssl

使用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

验证过程:

  1. 客户端向服务器请求加密连接,服务器用私钥加密网页后,连同crt(证书)、及证书颁发机构等信息一起返回给客户端,客户端收到证书后先向CA确认该证书是否在CA中登记过。如果没有则提示用户证书不安全,并询问用户是否继续访问
  2. 如果证书安全或用户要求继续访问,则客户端把crt(证书)中的csr(公钥)取出来,然后在客户端随机生成一串密钥和一个随机的加密算法,并使用csr(公钥)加密,返回给服务器端
  3. 服务器端使用key(私钥)把随机秘钥和加密算法解出来
  4. 至此,客户端和服务器都知道一个动态生成的密钥和对应的加密算法,以后客户端和服务器数据的传输就通过该动态生成的密钥和加密算法加密

说明:

  1. 私钥(key):服务器端生成,服务器自己使用,用于加密和解密数据
  2. 公钥(csr):服务器端生成,并上传到CA机构,由CA机构二次加密最终得到证书(crt),用于加密和解密数据
  3. 证书(crt):证书都是通过第三方认证机构颁发(比如CA机构)的,里面包含加密后的公钥,客户端把从服务器返回的证书和从CA机构获取的证书对比,确保服务器返回的证书没有被篡改

在协商随机秘钥(及对应的加密算法)的过程中使用的是非对称加密,通过公钥加密的数据只能使用私钥解出来(反之亦然),协商完成后双方都知道一个随机秘钥(及对应的加密算法),以后加密的数据都是用对称加密(加密解密用同一秘钥)

之所以在可以使用非对称加密来加密传输的数据的同时,还要协商一个对称加密的秘钥(及对应的加密算法),是因为非对称加密需要复杂的计算,要消耗大量的资源,所以希望使用对称加密,但是如果直接使用静态的对称加密秘钥又无法保障秘钥安全,所以才采用通过非对称加密协商一个动态生成的对称加密秘钥(及对应的加密算法)

HTTPS API

默认信任策略
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");
        sslContext.init(null, null, null);
        return sslContext.getSocketFactory();
    } catch (GeneralSecurityException e) {
        throw new AssertionError(); // The system has no TLS. Just give up.
    }
}

默认的SSLSocketFactory校验服务器的证书时,会信任设备内置的100多个根证书

此方法的特点:

自定义信任策略

如果要使用私有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");
        keyStore.load(null, null);
        keyStore.setCertificateEntry("anchor", cer);// 加载证书到密钥库中

        // 密钥管理器
        KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyFactory.init(keyStore, null);// 加载密钥库到管理器

        // 信任管理器
        TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustFactory.init(keyStore);// 加载密钥库到信任管理器

        // 初始化
        //要注意的是SecureRandom在Android4.4以前有漏洞(当用户没有提供用于产生随机数的种子时,程序不能正确调整偏移量,导致伪随机数生成器(PRNG)生成随机序列的过程可被预测),在Android4.4以前的系统最好使用第三方框架(如NoHttp,已修复该漏洞),否则要自己修复这个漏洞
        sslContext.init(keyFactory.getKeyManagers(), trustFactory.getTrustManagers(), new SecureRandom());

        return sslContext.getSocketFactory();
    } catch (GeneralSecurityException e) {
        throw new AssertionError(); // The system has no TLS. Just give up.
    }
}

如果要信任所有的HTTPS连接,可以使用null秘钥管理器和一个没有校验代码的TrustManager(不推荐)

sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new X509TrustManager() {
@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();
urlConnection.setSSLSocketFactory(getMySSLSocketFactory());
InputStream in = urlConnection.getInputStream();

在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配,则验证机制可以回调HostnameVerifier来确定是否应该允许此连接

HostnameVerifier hostnameVerifier = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
};
//使用
urlConnection.setHostnameVerifier(hostnameVerifier);

Volley

使用前先在build.gradle中添加依赖

compile 'com.android.volley:volley:1.1.1'

StringRequest

GET请求:

RequestQueue mQueue = Volley.newRequestQueue(context);

StringRequest stringRequest = new StringRequest("http://www.baidu.com",
        new Response.Listener<String>() {
            public void onResponse(String response) {
                // TODO
            }
        }, new Response.ErrorListener() {
            public void onErrorResponse(VolleyError error) {
                //TODO
            }
        });

mQueue.add(stringRequest);//执行请求

POST请求:

RequestQueue mQueue = Volley.newRequestQueue(context);

StringRequest stringRequest = new StringRequest(Method.POST, "http://127.0.0.1",
    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>();
        map.put("params1", "value1");
        map.put("params2", "value2");
        return map;
    }
};

mQueue.add(stringRequest);//执行请求

JsonObjectRequest

RequestQueue mQueue = Volley.newRequestQueue(context);

JsonObjectRequest jsonObjectRequest = new JsonObjectRequest("http://127.0.0.1", null,
        new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                // TODO
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // TODO
            }
        });

mQueue.add(jsonObjectRequest);

ImageRequest

RequestQueue mQueue = Volley.newRequestQueue(context);

//ImageRequest(URL,responseListener,maxwidth,maxheight,ColorProperties,errorListener),其中maxwidth、maxheight设成0表示不限制图片大小
ImageRequest imageRequest = new 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
            }
        });

mQueue.add(imageRequest);

ImageLoader

初始化

RequestQueue mQueue;
ImageCache mImageCache;
ImageLoader mImageLoader;

private void init(Context context) {
    mQueue = Volley.newRequestQueue(context);
    ImageCache.ImageCacheParams cacheParams = new ImageCache.ImageCacheParams(context, FileManager.CACHE_IMAGE_PATH_NEW);
    cacheParams.setMemCacheSizePercent(context, 0.2f);//缓存图片大小为原图的0.2倍
    mImageCache = new ImageCache(cacheParams);
    mImageLoader = new ImageLoader(mQueue, imageCache);//注意导的是Volley下的ImageLoader
}

或者使用自定义ImageCache

class VolleyImageCache implements ImageLoader.ImageCache {
    //LruCatch是Android提供的缓存工具
    private LruCache<String, Bitmap> mCache;

    public VolleyImageCache() {
        int maxCacheSize = 1024 * 1024 * 10;
        mCache = new LruCache<String, Bitmap>(maxCacheSize) {
            //测量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) {
        mCache.put(url, bitmap);
    }
}

使用

if (mImageCache.getBitmapFromMemCache(url) != null) {
    imageView.setImageBitmap(mImageCache.getBitmapFromMemCache(url));
} else {
    ImageLoader.ImageContainer container = mImageLoader.get(url, TransitionImageListener.obtain(imageView, R.drawable.loading_pic, R.drawable.default_pic), width, height);
    imageView.setTag(container);
}

NetworkImageView

在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 network_image_view = (NetworkImageView) findViewById(R.id.nivTestView);
network_image_view.setDefaultImageResId(R.drawable.default_pic);
network_image_view.setErrorImageResId(R.drawable.error_pic);
network_image_view.setImageUrl(url, mImageLoader);

案例: 多线程下载

原理

  1. Http的Range头字段指定每条线程从文件的什么位置开始下载,下载到什么位置为止,Range头需要服务器支持
  2. RandomAccessFile可以从文件指定位置开始写入

所以我们先计算每条线程要下载的Range,然后再通过RandomAccessFileseek函数指定写入位置即可

实现

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();
        connection.setConnectTimeout(15 * 1000);
        connection.setRequestMethod("GET");
        connection.connect();
        if (connection.getResponseCode() == 200) {
            size = (long) connection.getContentLength();
        } else {
            throw new IllegalStateException("连接失败");
        }

        RandomAccessFile raf = new RandomAccessFile(new File(fileDir), "rw");
        raf.setLength(size);
        raf.close();

        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);
            threadList.add(thread);
            thread.start();
        }
        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();
            conn.setConnectTimeout(15 * 1000);
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg," +
                    " 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, */*");
            conn.setRequestProperty("Referer", url);
            conn.setRequestProperty("Range", "bytes=" + start + "-" + end);// 设置获取实体数据的范围
            conn.setRequestProperty("Connection", "Keep-Alive");
            conn.connect();

            //如果服务器支持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");
                raf.seek(start);

                while ((len = is.read(buf)) != -1) {
                    raf.write(buf, 0, len);
                    progress += len;
                    callback.onProgress(progress);
                }
                raf.close();
                is.close();
                callback.onFinish();
            } else {
                callback.onError(new Exception("网络错误,请求失败,状态码为" + conn.getResponseCode()));
            }
        } catch (Exception e) {
            callback.onError(e);
        }
    }

    interface Callback {
        void onProgress(long progress);
        void onFinish();
        void onError(Exception e);
    }
}

消息异步

Handler

基本使用

public class MyHandlerActivity extends Activity{
    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg){
            switch(msg.what){
                case 1:
                    Toast.makeText(this,(String)msg.obj,0).show();
                    break;
                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 msg = Message.obtain();
            msg.what = 1;
            msg.obj = "Hello";
            handler.sendMessage(msg);
        }).start();
    }
}

除此以外,还有

handler.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);//延迟指定时间发消息

底层原理

使用HandlersendMessage等方法进行线程间通信实际上是把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
        Looper.prepare();

        mHandler = new Handler() {
        public void handleMessage(Message msg) {
            // process incoming messages here
        }
    };
        //执行轮询,在执行完任务后需要使用Looper.myLooper().quit()来停止轮询,否则可能会导致内存泄露
        Looper.loop();
    }
}

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");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

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 =
            SystemProperties.getInt("log.looper."
                    + Process.myUid() + "."
                    + Thread.currentThread().getName()
                    + ".slow", 0);

    boolean slowDeliveryDetected = false;

    //轮询
    for (;;) {
        //如果此时消息队列中有Message,那么next方法会立即返回该Message,如果此时消息队列中没有Message,那么next方法就会阻塞式地等待获取Message
        Message msg = queue.next(); // might block
        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) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long traceTag = me.mTraceTag;
        long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
        long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
        if (thresholdOverride > 0) {
            slowDispatchThresholdMs = thresholdOverride;
            slowDeliveryThresholdMs = thresholdOverride;
        }
        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)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }

        final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
        final long dispatchEnd;
        try {
            //msg.target是一个Handler
            msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (logSlowDelivery) {
            if (slowDeliveryDetected) {
                if ((dispatchStart - msg.when) <= 10) {
                    Slog.w(TAG, "Drained");
                    slowDeliveryDetected = false;
                }
            } else {
                if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                        msg)) {
                    // Once we write a slow delivery log, suppress until the queue drains.
                    slowDeliveryDetected = true;
                }
            }
        }
        if (logSlowDispatch) {
            showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                    + msg.callback + " what=" + msg.what);
        }

        msg.recycleUnchecked();
    }
}

我们可以通过Looper.myLooper()来获取当前线程绑定的Looper,也可以通过Looper.getMainLooper()获取UI线程的Looper

子线程是不能直接更新UI的,要在子线程更新UI,有以下方式:

new Thread(new Runnable() {
@Override
    public void run() {
    Log.e("TAG", "在子线程中创建handler,通过并指定绑定的Looper为MainLooper:"+Thread.currentThread().getName());

    Handler handler=new Handler(getMainLooper());
    handler.post(new Runnable() {
        @Override
        public void run() {
            Log.e("TAG", "使用子线程中创建handler往主线程发消息:"+Thread.currentThread().getName());
        }
    });

    }
}).start();

对于Toast、showDialog,其内部也是使用Handler向UI线程发起更新UI的message的,在子线程中使用Toast、showDialog我们只需要为子线程创建Looper然后直接使用就可以了,因为其内部已经为我们完成在主线程运行的操作了

new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        Toast.makeText(MainActivity.this, "run on thread"+Thread.currentThread().getName(), Toast.LENGTH_SHORT).show();
        Looper.loop();
    }
}).start();

最后,在执行完任务后需要使用Looper.myLooper().quit()来停止轮询,否则可能会导致内存泄露

内存泄露问题

这里主要讨论Handler的内存泄露问题

延时的Handler容易造成内存泄漏(Handler持有activity的引用,导致activity无法回收),解决方法有两种:

  1. 在退出activity时清空MessageQueue
public void onDestory(){
    handler.removeCallbacksAndMessages(null);
}
  1. 使用静态内部类+弱引用

在Java中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用,静态的内部类不会持有外部类的引用,所以把handler定义成静态内部类的话,不会持有Activity的引用,Activity可以随便回收,这时还要在handler中把对Activity的引用改成弱引用(GC在回收内存的时候会忽略调弱引用,只要对象没有被强引用指向(实际上多数时候还要求没有软引用),都能被GC回收)

public static MyHandler extends Handler{
    WeakReference<Activity> mWeakReference;
    public MyHandler(Activity activity){
        mWeakReference=new WeakReference<Activity>(activity);
    }
    @Override
    public void handleMessage(Message msg){
        final Activity activity=mWeakReference.get();
        if(activity!=null){
            if (msg.what == 1){
                // TODO
            }
        }
    }
}

AsyncTask

RequestQueue mQueue = Volley.newRequestQueue(context);
new AsyncTask<String, Integer, Bitmap>(){

    //在doInBackground之前执行
    protected void onPreExecute() {
        //在主线程执行
    }

    protected Bitmap doInBackground(String... args1) {
        //在子线程执行
        Bitmap bitmap = null;
        ImageRequest imageRequest = new ImageRequest(args1[0],
        new Response.Listener<Bitmap>() {
            @Override
            public void onResponse(Bitmap response) {
                bitmap = response;
            }
        }, 0, 0, Bitmap.Config.RGB_565, null);
        mQueue.add(imageRequest);
        for (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");

JSON解析

JSONObject和JSONArray

JSONObject.toBean的数据是用{ }来表示的,如:{ "id" : "123", "name" : "小明"}

JSONArray是JSONObject的数组,用[ ]表示,如[ { "id" : "123", "name" : "小明"}, { "id" : "124", "name" : "小红"} ]

//创建,根据json字符串的不同选择创建不同的对象
JSONObject jsonObject  = new JSONObject(String str);
JSONArray jsonArray = new JSONArray(String str);

//JSONArray通过index获取JSONObject
JSONObject jsonObject = (JSONObject)jsonArray.get(0);
//获取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>();
list.add("java");
list.add("android");
JSONArray jsonarray = JSONArray.fromObject(list);

Gson

使用前先在build.gradle中添加依赖

implementation 'com.google.code.gson:gson:2.8.5'

使用

Person person = new Person(11,"小明",“Man”);//Person是一个JavaBean
String jsonStr = new Gson.toJson(person, Person.class);//Object转json
Person person2 = new Gson.fromJson(jsonStr, Person.class);//json转Object

/*
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的默认参数
Gson gson2 = new GsonBuilder().setVersion(1.0)
            .excludeFieldsWithoutExposeAnnotation()
            .create();

自定义View

生命周期

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;某些情况下,事件还可以从子控件回传到父控件中

父控件要拦截事件,则可以重写onInterceptTouchEvent(),并返回true,这样子控件就不会得到该事件,但子控件可以通过getParent().requestDisallowInterceptTouchEvent()来禁止拦截,该函数优先级比onInterceptTouchEvent()

事件拦截与消费

我们给LinearLayout重写dispatchTouchEventonInterceptTouchEventonTouchEvent,并给它设置OnTouchListenerOnClickListenerOnLongClickListener

初始状态,没有任何拦截、消费的情况下:

@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);
}
linearLayout.setOnTouchListener((view, motionEvent) -> {
    System.out.println("OnTouchListener action=" + motionEvent.getAction());
    return false;
});

linearLayout.setOnClickListener((view) -> System.out.println("OnClickListener"));

linearLayout.setOnLongClickListener(view -> {
    System.out.println("OnLongClickListener");
    return false;
});

打印(ACTION_DOWN = 0ACTION_UP = 1ACTION_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;
}

这时OnLongClickListenerOnClickListener都不会触发

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;
}

这时OnLongClickListenerOnClickListener可以正常触发,说明点击和长按事件是由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

linearLayout.setOnLongClickListener(view -> {
    System.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

把代码恢复到初始状态,这时,如果我们在linearLayoutonInterceptTouchEvent中返回true,给linearLayout添加一个子控件button,并给重写button的dispatchTouchEventonInterceptTouchEventonTouchEvent,设置OnTouchListenerOnClickListenerOnLongClickListener

@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

如果父ViewOnInterceptTouchEvent中返回true,则子View所有事件都不会接收到(子类可以通过getParent().requestDisallInterceptRouchEvent(true)来禁止父View拦截),此时子ViewonTouchEvent事件交由父ViewonTouchEvent处理

如果onInterceptTouchEventACTION_DOWN的时候返回false,后面的ACTION_MOVEACTION_UP都不会再调用onInterceptTouchEvent(如果在ACTION_DOWN的时候返回true,后面的事件还能收到)

上面任何一个函数返回true,代表消费了事件,则后面的函数都不会继续执行,但OnLongClickListenerOnClickListener是通过super.onTouchEvent实现的,只要调用了super.onTouchEvent,无论有没有消费事件,都可以触发

上面的图画得不够准确,OnLongClickListener会在触摸一段事件后触发,触发后如果用户还在长按,还会继续执行ViewGroup控件dispatchTouchEventView控件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 decor = (ViewGroup) activity.getWindow().getDecorView();
// 拿到第一个子view-decorChild
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
ViewGroup.LayoutParams params = decorChild.getLayoutParams();
// 删除子view-decorChild
decor.removeView(decorChild);
// 把子view-decorChild添加到SwipeBackLayout(FragmentLayout)下
this.addView(decorChild);
// 把SwipeBackLayout(FragmentLayout)添加到DecorView下
decor.addView(this, params);

监听事件

ViewGroup decorChild;
boolean 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:
                downX = ev.getX();
                downY = ev.getY();
                currentX = downX;
                currentY = downY;
                lastX = downX;
                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) {
                        downX = ev.getX();
                        downY = ev.getY();
                        currentX = downX;
                        currentY = downY;
                        lastX = downX;
                        canSwipe = true;
                        return true;
                    } else {
                        ignoreSwipe = true;
                    }
                }
                break;
        }
    }
    if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
        ignoreSwipe = false;
    }
    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:
                downX = event.getX();
                downY = event.getY();
                currentX = downX;
                currentY = downY;
                lastX = downX;
                break;
            case MotionEvent.ACTION_MOVE:
                currentX = event.getX();
                currentY = event.getY();
                float dx = currentX - lastX;
                if (getContentX() + dx < 0) {
                    decorChild.setX(0);
                } else {
                    decorChild.setX(decorChild.getX() + dx);
                }
                lastX = currentX;
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                canSwipe = false;
                //回弹
                //...
                break;
            default:
                break;
        }
    }
    return super.onTouchEvent(event);
}

Shape

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实现其他效果

//画菱形
Path path = new Path();
path.moveTo(50, 0);//单位都是px,实际使用需转成dp
path.lineTo(0, 50);
path.lineTo(50, 100);
path.lineTo(100, 50);
path.close();

ShapeDrawable mDrawables = new ShapeDrawable(new PathShape(path, 100, 100));

RectShape

RectShape

RectShape rectShape = new RectShape();
ShapeDrawable drawable = new ShapeDrawable(rectShape);
drawable.getPaint().setColor(Color.BLUE);
drawable.getPaint().setStyle(Paint.Style.FILL);

其子类有

自定义属性

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代码中获取属性值

TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.MyAttr);//attrs就是当前自定义view的构造函数中的AttributeSet参数
name = typedArray.getString(R.styleable.MyAttr_name, "");//第二个参数为不匹配时的默认值
id = typedArray.getInt(R.styleable.MyAttr_id, -1);
itemBg = typedArray.getResourceId(R.styleable.MyAttr_portrait, -1);

typedArray.recycle();//用完记得回收

Service

配置

<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>

startService

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

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() 期间绑定和取消绑定,因为每一次生命周期转换都会发生这些回调,这样反复绑定与解绑是不合理的

Context与Service交互

要在客户端(Activity)使用Service的公共方法,有以下思路:

IBinder

自定义IBinder,用于Service(服务端)与客户端相同的进程中运行时的通讯

public class MyService extends Service{
    private MyBinder binder = new MyBinder();

    //自定义Binder,有一个返回服务器实例的方法
    public class MyBinder extends Binder {
        MyService getService() {
            return MyService.this;
        }
    }

    //在onBind返回自定义的Binder,就可以通过该Binder获取Service实例
    public IBinder onBind(Intent intent) {
        return binder;
    }

    //公共方法
    public String getName(){
        retrun this.getClass().getName();
    }
}

public class MainActivity extends Activity{
    ServiceConnection conn;
    private MyService myService;

    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent(this, MyService.class);
        conn = new ServiceConnection() {
            //绑定成功的回调
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                MyService.MyBinder binder = (MyService.MyBinder) service;
                myService = binder.getService();
            }
            //当取消绑定的时候被回调。但正常情况下是不被调用的,它的调用时机是当Service服务被意外销毁时,例如内存的资源不足时这个方法才被自动调用
            @Override
            public void onServiceDisconnected(ComponentName name) {
                myService=null;
            }
        };
        bindService(intent, conn, Service.BIND_AUTO_CREATE);
    }

    protected void onDestory(){
        if(myService != null){
            myService = null;
            unbindService(conn);
        }
    }
}

Messenger

使用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:
                    //接收到来自客户端的信息
                    Log.i("TAG", "接收到客户端的message");
                    //回复客户端
                    Messenger client=msg.replyTo;
                    Message replyMsg=Message.obtain(null,MessengerService.MSG_2);
                    Bundle bundle=new Bundle();
                    bundle.putString("reply","ok");
                    replyMsg.setData(bundle);
                    try{
                        client.send(replyMsg);
                    }catch(RemoteException e){
                        e.printStackTrace();
                    }
                    break;
                default: break;
            }
        }
    }

    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }
}

客户端:

public class ActivityMessenger extends Activity {
    Messenger mService = null;
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            //通过服务端传递的IBinder对象,创建相应的Messenger,通过该Messenger对象与服务端进行交互
            mService = new Messenger(service);
        }

        public void onServiceDisconnected(ComponentName className) {
            mService = null;
        }
    };

    //用于接收服务器端的回复
    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:
                    //得到服务器回复
                    Log.i("TAG",msg.getData().getString("reply"));
                    break;
                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 msg = Message.obtain(null, MessengerService.MSG_1, 0, 0);
            //把接收服务器端的回复的Messenger通过Message的replyTo参数传递给服务端
            msg.replyTo=mRecevierReplyMsg;
            try{
                //向服务器端发消息
                mService.send(msg);
            }catch (RemoteException e){
                e.printStackTrace();
            }
        }
    }

    protected void onDestory(){
        if(mService!=null){
            unbindService(mConnection);
            mService = null;
        }
    }
}

为了测试跨进程通信,还要对service配置android:process=":remote"

<service android:name=".MessengerService"
         android:process=":remote"
        />

AIDL

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) {
        id = in.readInt();
        name = in.readString();
        className = in.readString();
    }

    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) {
        dest.writeInt(id);
        dest.writeString(name);
        dest.writeString(classCame);
    }

    public void readFromParcel(Parcel source) {
        id = source.readInt();
        name = source.readString();
        className = source.readString();
    }

    @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 {
            studentList.add(student);
        }

        @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请求,得到结果再返回,但在我们看来,调用方式是一样的,我们直接调用之前声明的方法即可
        aidlProxy = IMyAidl.Stub.asInterface(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        mAidl = null;
    }
};

public void click(View v) {
    Intent intent = new Intent();
    intent.setAction("com.myapp.app.service.MyAidlService");
    //使用显式intent
    intent.setPackage("com.myapp.app.service");
    bindService(intent, conn, BIND_AUTO_CREATE);
}

前台服务

由于Service的优先级很低,所以在手机灭屏一段时间后,service很可能就被系统回收了,为了保证service不会被系统回收,我们需要将service设置为前台服务,在这个时候状态栏上会出现一个通知,通过这个通知我们可以做一些操作

public class ForegroundService extends Service {
private static final int NOTIFICATION_DOWNLOAD_PROGRESS_ID = 0x0001;
private boolean isRemove=false;//是否需要移除

public void createNotification(){
    //使用兼容版本
    NotificationCompat.Builder builder=new NotificationCompat.Builder(this);
    //设置状态栏的通知图标
    builder.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!!!");
    //创建通知
    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();
        }
        isRemove=true;
    }else {
        //移除前台服务
        if (isRemove) {
            stopForeground(true);
        }
        isRemove=false;
    }

    return super.onStartCommand(intent, flags, startId);
}
public void onDestroy() {
        //移除前台服务
        if (isRemove) {
            stopForeground(true);
        }
        isRemove=false;
        super.onDestroy();
    }
    public IBinder onBind(Intent intent) {
        return null;
    }

Android 5.0以上的隐式启动问题

显式启动

Intent intent = new Intent(this,ForegroundService.class);
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");
serviceIntent.setPackage(getPackageName());//设置应用的包名
startService(serviceIntent);

2、将隐式启动转换为显示启动

public static Intent getExplicitIntent(Context context, Intent implicitIntent) {
    // Retrieve all services that can match the given intent
    PackageManager pm = context.getPackageManager();
    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 serviceInfo = resolveInfo.get(0);
    String packageName = serviceInfo.serviceInfo.packageName;
    String className = serviceInfo.serviceInfo.name;
    ComponentName component = new ComponentName(packageName, className);
    // Create a new intent. Use the old one for extras and such reuse
    Intent explicitIntent = new Intent(implicitIntent);
    // Set the component to be explicit
    explicitIntent.setComponent(component);
    return explicitIntent;
}

//调用
public void startMyService(){
    Intent mIntent=new Intent();//辅助Intent
    mIntent.setAction("com.myapp.ForegroundService");
    final Intent serviceIntent=new Intent(getExplicitIntent(this,mIntent));
    startService(serviceIntent);
}

BroadcastReceiver

注册

静态注册:

<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>

动态注册(必须在退出应用时解注册,否则会导致内存泄漏):

MyBroadcastReceiver myBroadcastReceiver;

protected void onResume(){
    super.onResume();

    myBroadcastReceiver = new MyBroadcastReceiver();

    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);

    //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();
        AsyncTask<String, Integer, String> asyncTask = new AsyncTask<String, Integer, String>() {
            @Override
            protected String doInBackground(String... params) {
                StringBuilder sb = new StringBuilder();
                sb.append("Action: " + intent.getAction() + "\n");
                sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\n");
                pendingResult.finish();
                return data;
            }
        };
        asyncTask.execute();
    }
}

发送广播

广播的类型主要分为5类: - Normal Broadcast 普通广播 - System Broadcast 系统广播 - Ordered Broadcast 有序广播 - Sticky Broadcast 粘性广播 - Local Broadcast App应用内广播

普通广播

发送广播的时候建议:

Intent intent = new Intent();
intent.setAction("com.myapp.MyBroadcastReceiver");
intent.setPackage(getPackageName());//指定该广播接收器所在的包名
//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_CHANGEDIntent.ACTION_BATTERY_CHANGEDIntent.ACTION_BOOT_COMPLETEDIntent.ACTION_HEADSET_PLUGIntent.ACTION_REBOOT

有序广播

按照android:priority的顺序接收广播,优先级高的先接收到,同样优先级的情况下,动态注册的优先;先接收到的可以对广播进行拦截、更改等操作

sendOrderedBroadcast(intent);

//BroadcastReceiver中:
abortBroadcast();//中断广播,此方法只能在接收到有序广播的时候使用,其他广播下使用会报错

粘性广播

由于在Android5.0(API 21)中已经失效,所以不建议使用,在这里也不作过多的总结

App应用内广播

用于防止由于注册的广播名称相同导致的冲突问题(安全性、效率性问题),应用内广播无法跨进程通信

使用应用内广播需将静态注册的receiver的exported属性设置为false,使得广播仅在本应用(本进程)中使用

MyBroadcastReceiver myBroadcastReceiver = new MyBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(android.net.conn.CONNECTIVITY_CHANGE);

LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
lbm.registerReceiver(myBroadcastReceiver, intentFilter);//注册应用内广播

Intent intent = new Intent();
intent.setAction("com.myapp.MyBroadcastReceiver");
lbm.sendBroadcast(intent);

lbm.unregisterReceiver(myBroadcastReceiver);//解注册应用内广播

对于不同注册方式的广播接收器回调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

Provider

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{
    UriMatcher static matcher;//常用的判断URI是否匹配的类
    MySQLiteHelper helper;//ContentProvider的增删改查一般交由数据库处理
    static{
        matcher = 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);//"#"表示任意数字
    }

    @Override
    public boolean onCreate() {
        help = new MySQLiteHelper(getContext());
        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) {
            type = "vnd.android.cursor.dir/t_user";// dir代表多行数据
        } else if (code == 2) {
            type = "vnd.android.cursor.item/t_user";// item单行
        }
        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){
            cursor = help.getReadableDatabase().query( "t_user",  new String[] { "id", "name", "sex" }, selection, selectionArgs, null, null, sortOrder);
        }else if(code == 2){
            long id = ContentUris.parseId(uri);//取出"#"处的数字
            cursor = help.getReadableDatabase().query("t_user", new String[] { "id", "name", "sex" }, "id=?", new String[] { String.valueOf(id) }, null, null, sortOrder);
        }
        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);
            help.getWritableDatabase().delete("t_user", "id=?", new String[] { id + "" });
        }
        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);
            row = help.getWritableDatabase().update("t_user", values, "id=?", new String[] { id + "" });
        }
        return row;
    }
}

数据使用端(其他Activity)

Uri uri = Uri.parse("content://com.myapp.MyProvider/t_user");
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"));
    Log.i("TAG","id="+id+",name="+name+",sex="+sex);
}
cursor.close();

Observer

使用ContentObserver内容观察者可以监听ContentProvider相关的数据变化;如果自定义的provider想通知监听的对象(ContentObserver),需在其对应方法update /insert/delete时,显式地调用this.getContentReslover().notifychange(uri,null),如果不显式调用,即使注册了ContentObserver,也不会收到回调(onChange)

public class ObserverActivity extends Activity {
    ContentObserver observer;
    private Handler handler = new Handler() {
        public void handleMessage(Message msg) {
             switch (msg.what) {
                 case 1:
                    Log.i("TAG",msg.obj.toString());
                    break;
                default: break;
             }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.main);

        observer = new MyObserver(this, handler);
        //注册内容观察者,监听短信数据变化,第二个参数代表是否精确匹配,像类似带"#"或者其他查询字段的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) {
        ContentResolver resolver = context.getContentResolver();
        Cursor c = resolver.query(Uri.parse("content://sms"), null, null, null, null);
        if(c != null){
            StringBuilder sb = new StringBuilder();
            while (c.moveToNext()) {
                sb.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");}
            c.close();
            Message msg = new Message();
            msg.what = 1;
            msg.obj = sb.toString();//将读到的信息使用msg,传递给activity
            handler.sendMessage(msg);
        }
    }
}

案例: 自动填写短信验证码

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;
        //获取短信验证码
        ContentResolver contentResolver = activity.getContentResolver();
        cursor = contentResolver.query(Uri.parse(SMS_URI_INBOX), new String[] { "_id", "address", "body", "read" }, "body like ? and read=?", new String[] { "%验证码%", "0" }, "date desc");
        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()){
                    smsContent = m.replaceAll("").trim().toString();
                    listener.onResult(smsContent);
                }
            }
        }
    }

    // 短信回调接口 
    public interface SmsListener {
        void onResult(String smsContent);
    }
}

动画

Tween动画

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);
Animation anim = AnimationUtils.loadAnimation(this, R.anim.anim);
imageView.startAnimation(anim);

JAVA创建Tween动画

LinearLayout ll = (LinearLayout) findViewById(R.id.ll);
AnimationSet set = new AnimationSet();
AlphaAnimation alpha = new AlphaAnimation(0, 1);// 0---->1从透明到不透明
ScaleAnimation scale = new ScaleAnimation(0, 2, 0, 2);
RotateAnimation rotate = new RotateAnimation(0, 360, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
TranslateAnimation translate = new TranslateAnimation(0, 200, 0, 300);
set.addAnimation(alpha);
set.addAnimation(scale);
set.addAnimation(rotate);
set.addAnimation(translate);
set.setDuration(3000);//统一设置动画持续时间
ll.startAnimation(set);

帧动画

像播放幻灯片一样,传一组图片进去,然后依次循环播放,可以设置每一张图片的播放时间。帧动画可以通过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代码创建

AnimationDrawable anim = new AnimationDrawable();
for (int i = 1; i <= 6; i++) {
    int id = getResources().getIdentifier("run_" + i, "mipmap", getPackageName());
    Drawable drawable = getResources().getDrawable(id);
    anim.addFrame(drawable, 150);
}
anim.setOneShot(false);
imageView.setImageDrawable(anim);
anim.start();

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);
    imageView.setImageResource(R.drawable.animation_list);
    AnimationDrawable anim = (AnimationDrawable) imageView.getBackground();
    anim.start();
    //anim.stop();//停止动画
}

属性动画

属性动画(ObjectAnimator),继承自数值发生器(ValueAnimator),可以改变view的实际参数

ValueAnimator:

ValueAnimator animator = ValueAnimator.ofFloat(0, 360);
animator.setDuration(1000);
animator.setInterpolator(new AccelerateInterpolator());
animator.setRepeatCount(1);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        //value就是从0到360的数字,只不过每加一延时0.36毫秒(360/1000)
        float value = (float)valueAnimator.getAnimatedValue();
        imageView.setRotationY(value);
    }
    });
animator.start();

ObjectAnimator其实就是帮我们实现了常用动画的ValueAnimator

属性动画可以通过ObjectAnimator.ofFloat()ObjectAnimator.ofInt()ObjectAnimator.ofObject()等静态工厂方法创建,或者通过view.animate()创建,多个属性动画(复合动画)用AnimatorSet

AnimatorSet set = new AnimatorSet();
ObjectAnimator alpha = ObjectAnimator.ofFloat(view,"alpha",1,0,1);
//JAVA代码中的单位是px,实际应用中应该根据公式转成dp
ObjectAnimator rotationX = ObjectAnimator.ofFloat(view,"rotationX",0,270,50);
ObjectAnimator translationX = ObjectAnimator.ofFloat(tv, "translationX", 0, 200, -200,0);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 2f, 1f);

set.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();

//通过view.animate()创建
view.animate().translationXBy(view.getWidth())
              .translationYBy(view.getWidth())
              .setDuration(2000)
              .setInterpolator(new BounceInterpolator())
              .start();

//除了使用系统提供的变换方式,还可以自己指定动画的移动路径
Path path = new Path();
path.cubicTo(0.2f, 0f, 0.1f, 1f, 0.5f, 1f);
path.lineTo(1f, 1f);
ObjectAnimator animator = ObjectAnimator.ofFloat(iv, view.X, view.Y, path);
animator.setDuration(2000);
animator.setRepeatCount(1);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.start();

Interpolator

插值器(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中使用:

animation.setInterpolator(new AccelerateInterpolator());

//除了xml中对应的Interpolator类,还有JAVA代码特有的PathInterpolator
//PathInterpolator是按照贝塞尔曲线运动的,官方提供了一个工具类生成PathInterpolator,这样就不用自己实现贝塞尔曲线了
//三阶贝塞尔曲线
Path path = new Path();
path.cubicTo(0.2f, 0f, 0.1f, 1f, 0.5f, 1f);
path.lineTo(1f, 1f);

ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 500);
animator.setInterpolator(PathInterpolatorCompat.create(path));//通过工具类创建
animator.start();

Activity转场动画

通过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);
intent = new Intent(this, CActivity.class);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this,
        Pair.create(view, "share"),
        Pair.create(fab, "fab"),
        Pair.create(txName, "user_name"))
        .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()

Intent intent = new Intent(this, CActivity.class);
startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());

CircularReveral

这是Android5.0推出的新的动画框架,可以给View做一个圆角的揭露效果

//五个参数分别是View,中心点坐标x、y,开始半径,结束半径
Animator anim = ViewAnimationUtils.createCircularReveal(view, 0, 0, 0, (float) Math.hypot(rect.getWidth(), rect.getHeight()));
anim.setDuration(2000);
anim.start();

矢量动画

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-vectoranimated-vector的drawable指定静态时显示的图案,target是对外部xml的引用,targetobjectAnimator)的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指向的是一个objectAnimatorobjectAnimator表示动画的变换方式

<!-- 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指向的是一个vectorvector是元素的矢量资源(静态时显示的图案),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) {
    Drawable drawable = imgBtn.getDrawable();
    ((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>

Fragment

生命周期

[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()
        Bundle bundle = getArguments();
    }

    @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>

动态添加与删除

FragmentManager frgmentManager = getFragmentManager();// v4中,getSupportFragmentManager
FragmentTransaction transaction = frgmentManager.benginTransatcion();
transaction.addToBackStack(null);//支持返回键,点击返回时,回到上一个frgment,否则点返回直接退出app
transaction.add(R.id.containerViewId, fragment1);//添加,containerView一般是FrameLayout
//如果在FragmentA中的EditText填了一些数据,当切换到FragmentB时,如果希望会到A还能看到数据,就用add加hide和show,如果不需要保存用户操作,就用add和remove
transaction.hide(fragment1);//隐藏,搭配add使用
transaction.show(fragment1);//显示,搭配add使用
transaction.setBreadCrumbShortTitle(R.string.title);//添加标题


//transaction.replace(R.id.containerViewId, fragment2);//替换,和add、remove一样,会销毁布局容器内的已有视图,这样会导致每次切换Fragment时都会重新初始化,类似EditText中已输入的数据不会被保留

//在不考虑回退栈的情况下,remove会销毁整个Fragment实例,而detach则只是销毁其视图结构,实例并不会被销毁,如果当前Activity一直存在,那么在不希望保留用户操作的时候,你可以优先使用detach
//transaction.remove(fragment2);//删除
//transaction.detach(fragment2);//取消挂载


transaction.commit();//提交后才生效

//可以手动返回上一个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);

    FragmentManager fragmentManager = getSupportFragmentManager();

    //防止fragment产生重叠问题
    if (savedInstanceState == null) {
        fragment1 = new Fragment1();
        fragment2 = new Fragment2();

        fragmentManager.beginTransaction()
                .add(R.id.containerViewId, fragment1, Fragment1.class.getName())
                .add(R.id.containerViewId, fragment2, Fragment2.class.getName())
                .commit();
    } else {
        fragment1 = (Fragment1) fragmentManager.findFragmentByTag(Fragment1.class.getName());
        fragment2 = (Fragment2) fragmentManager.findFragmentByTag(Fragment2.class.getName());
    }
    
    fragmentManager.beginTransaction()
            .show(fragment1)
            .hide(fragment2)
            .commit();
}

最好在Fragment中再判断一次Parent是否为空

private View mRoot;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    if(mRoot==null){
        mRoot = Inflater.inflate(R.layout.fragment1, container, false);
    } 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);
    isViewCreated = true;
    lazyLoad();
}

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    if (isVisibleToUser) {
        lazyLoad();
    }
}

private void lazyLoad() {
    //只有当view创建好并且对用户可见,而且没有正在加载数据时才开始加载数据
    if (isViewCreated && getUserVisibleHint() && !isLoadingData) {
        isLoadingData = true;
        // TODO 网络请求等操作,一般新建线程来做
        //数据加载完毕,恢复标记,防止重复加载
        isViewCreated = false;
        isLoadingData = false;
    }
}

其他

横竖屏切换

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",横竖屏切换后,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次

JAVA与javascript互调

先准备一个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= (WebView) findViewById(R.id.web_view);
    button= (Button) findViewById(R.id.bt_button);

    WebSettings webSettings = webView.getSettings();
    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);//隐藏原生的缩放控件

    //使得打开网页时不调用系统浏览器, 而是在本WebView中显示
    webView.setWebViewClient(new WebViewClient(){
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    });

    //文件放在src/main/assets/下时,URL为file:///android_asset/xxx
    webView.loadUrl("file:///android_asset/myweb.html");
    button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //无参数调用javascript的函数
                webView.loadUrl("javascript:actionFromJAVA()");

                String param = "JAVA";
                //有参数调用javascript的函数
                webView.loadUrl("javascript:actionFromJAVAWithParam(" + "\'" + param + "\'" + ")");
            }
        });
}

javascript调JAVA需要为webView设置JavaScriptInterface

//第二个参数表示标识,javascript调JAVA函数时方法名前缀就是这个标识,如window.myweb.actionFromJs(),最前面的window是固定写法,然后是标识,最后才是JAVA的方法名,该方法就是第一个参数的Object中实现了的方法,且此方法要有@JavascriptInterface注解
webView.addJavascriptInterface(new MyObject(),"myweb");
//要先addJavascriptInterface再loadUrl,JavascriptInterface才会生效
webView.loadUrl("file:///android_asset/myweb.html");

class MyObject{
    @JavascriptInterface
    actionFromJs(){
        Log.i("TAG", "javascript调用了JAVA的函数");
        //该函数不一定是主线程调,更新UI应该用runOnUiThread
        runOnUiThread(new Runnable() {
                public void run() {
                    Toast.makeText(MyActivity.this, "js调用了Native函数", Toast.LENGTH_SHORT).show();
                }
            });

    }

    @JavascriptInterface
    actionFromJsWithParam(String arg){
        Log.i("TAG", "javascript调用了JAVA的函数,参数是"+arg);
    }
}

浏览器跳转app

浏览器跳转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

Intent intent = getIntent();
String action = intent.getAction();
if(Intent.ACTION_VIEW.equals(action)){
    Uri uri = intent.getData();
    if(uri != null){
        String data = uri.getQueryParameter("data");
    }
}

夜间模式

官方的夜间模式支持只能在Android 6.0以上使用

  1. 添加依赖
dependencies {
    compile 'com.android.support:appcompat-v7:23.4.0'
}
  1. 让应用的主题继承自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>
  1. 为不同的模式设置不同的colorsstyle

res下新建values-night文件夹,当切换夜间主题时,会读取values-night中的style.xmlcolors.xml

  1. 指定主题

在app初始化或者Application中用设置

AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);

在应用中设置,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());

Intent

Action

要执行的动作,系统提供的有: - 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");
//也可以写成
Intent intent = new Intent();
intent.setAction("com.myapp.intent.MY_ACTION");
startActivity(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

系统提供的有: - 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:

Intent intent = new Intent(); intent.setAction(Intent.ACTION_MAIN);// 添加Action属性 intent.addCategory(Intent.CATEGORY_HOME);// 添加Category属性
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>

Data

执行动作所需的数据

用浏览器打开指定网页:

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);

打开系统拨号界面并指定电话号码:

Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:12345678"));
startActivity(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>

模仿浏览器打开应用

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("myapp://abcdefg.me:8080/fromweb/path1?data=213"));
startActivity(intent);

Extras

执行动作所需的附加信息

用浏览器搜索指定内容:

Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.putExtra(SearchManager.QUERY, "android");
startActivity(intent);

Type

通过MIME调用其他应用打开对应格式的文件

Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "video/x-msvideo");
startActivity(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]))
            type = MIME_MapTable[i][1];
    }
    return type;
}

Component

Intent intent = new Intent();
//方法1,指定context、class
intent.setComponent(new ComponentName(getApplicationContext(), MyActivity.class));

//方法2,指定context、action
intent.setComponent(new ComponentName(getApplicationContext(), "com.myapp.intent.MyActivity"));

//方法3,用于匹配其他包的目标,指定包名、全类名
intent.setComponent(new ComponentName("com.myapp.other", "com.myapp.other.OtherActivity"));

FLAG

FLAG可以指定Activity的启动方式(参考前面的”Activity的四种加载模式”)

在Service等非Activity中启动Activity的时候,必须指定FLAG_ACTIVITY_NEW_TASK

intent.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)

SystemService

使用context.getSystemService可以获取各种系统管理工具,如

Vibrator mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);

服务有:

动态权限获取

通过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) {
                        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CALL_PHONE}, 1001);//在app内弹框
                    }
                })
                .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) {
                        Intent intent = new Intent();
                        intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                        Uri uri = Uri.fromParts("package", getPackageName(), null);
                        intent.setData(uri);
                        context.startActivity(intent);//去设置中打开权限
                    }
                })
                .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) {
        fragment.requestPermissions(new String[]{permission}, requestCode);
    }
}

public static void getPermissionInActivity(Activity activity, String permission, int requestCode) {
    if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {
        activity.requestPermissions(new String[]{permission}, requestCode);
    }
}

通知

NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 

//创建点击通知时发送的intent
Intent resultIntent = new Intent(context, MainActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);//点击通知打开Activity,在Activity中按返回键时回到MainActivity,而不是回到主屏幕
stackBuilder.addParentStack(MainActivity.class);//添加Activity到返回栈
stackBuilder.addNextIntent(resultIntent);//添加Intent到栈顶
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

//创建删除通知时发送的intent
Intent deleteIntent = new Intent(context, NotificationService.class);
deleteIntent.setAction("xxx");//删除时往Service中发消息
PendingIntent deletePendingIntent = PendingIntent.getService(context, 0, deleteIntent, 0);


//创建通知
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) {
    NotificationChannel channel = new NotificationChannel(channelID, channelName, NotificationManager.IMPORTANCE_HIGH);//不同的重要等级有不同的显示方式
    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.setSound(null,null);
    channel.setVibrationPattern(null);*/
    channel.setShowBadge(false);//是否在长按桌面图标时显示此channel的通知

    notificationManager.createNotificationChannel(channel);//创建Channel

    notificationBuilder.setChannelId(channelID);//设置channelID
}

//发送通知
notificationManager.notify(notificationID, notificationBuilder.build());

NDK

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){
        Log.i("TAG", "C调用了JAVA");
        return (i + j);
    }

3、生成对应的头文件:

打开AS Terminal(或者cmd也可以),在AS Terminal中切换目录至app/build/intermediates/classes/debug,输入javah -jni com.myapp.jnidemo.JniUtilsjavah -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 *envjobject 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_包名_类名_方法名"的格式
JNIEXPORT jstring JNICALL Java_com_myapp_jnidemo_JniUtils_javaCallC(JNIEnv *env, jobject obj, jstring str){
    return (*env)->NewStringUTF(env,"I am from C!");
}

//C调JAVA的函数,因为要用到JNIEnv,所以一般用于JAVA调C,然后C回调JAVA
//jobject就是调用该函数的类对象,这里是JniUtils
JNIEXPORT void JNICALL Java_com_myapp_jnidemo_JniUtils_cCallJavaCallback(JNIEnv *env, jobject obj){
    //1.得到字节码,第二个参数为"全类名",把"."改成"/"
    jclass jclazz = (*env)->FindClass(env, "com/myapp/jnidemo/JniUtils");
    //2.得到方法ID,第四个参数为方法签名,可以在debug目录下使用"javap -s 全类名",来得到类的所有方法签名(因为函数可以重裁,只有方法名还不知道要调哪个方法,需指定方法签名)
    jmethodID methodID = (*env)->GetMethodID(env, jclazz, "cCallJava", "(II)I");
    //3.实例化对象
    jobject jobj = (*env)->AllocObject(env, jclazz);
    //4.调用方法
    jint result = (*env)->CallIntMethod(env, jobj, methodID, 10, 20);
}

5、配置编译参数

配置App目录下的build.gradle,添加ndk节点:

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        ndk{
            moduleName "MyJniLib"//生成的so名字
            abiFilters "armeabi", "armeabi-v7a", "x86"//输出指定三种abi体系结构下的so库,让代码可以在这三种环境下运行
        }
    }

    debug{
        ndk{
            moduleName "MyJniLib"
            abiFilters "armeabi", "armeabi-v7a", "x86"
        }
    }
}

配置gradle.properties,让Android Studio兼容旧版的NDK

android.useDeprecatedNdk=true

6、至此JNI已配置完成,可以在JAVA中使用NDK了

JniUtils jniUtils = new JniUtils();
String str = jniUtils.javaCallC(“java”);
jniUtils.cCallJavaCallback();
Log.i("TAG",str)

热更新

参考

腾讯项目 Tinker

阿里项目 AndFix

Material Design

参考 Material Design中文文档

https://material.io/

设计模式

官方MVP使用案例

参考文章

解读Android官方MVP项目单元测试

选择恐惧症的福音!教你认清MVC,MVP和MVVM

学习路线

Android官方培训课程中文版

老罗的Android之旅(总结)

Android framework源码(github)

Android源码(googlesource)

安卓开发常用的第三方库集合

微信小程序文档