代码总结(中)

数据存储部分

那些在内存中的瞬时数据存储到存储设备中,保证即使手机或电脑关机了,这些数据也不会丢失.

文件存储

不对存储的内容进行任何格式化的更改,所有数据都是原封不动的保存到文件当中,比较适合用来存储一些简单的文本数据或二进制数据.
文件存储最核心的技术就是Context类中提供的openFileInput()和openFileOutput()方法,之后利用java的各种流来进行读写操作,只要熟练掌握java的各种流的基本操作,应该不成问题,此外关于节点流 ,处理流(包装流),缓冲流的相关知识一定要熟记,各种方法的参数的类型,尤其是Context类中提供的openFileInput()和openFileOutput()这两个方法的参数的含义一定要搞清楚!!!

public class MainActivity extends AppCompatActivity {
    private EditText mEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mEditText = findViewById(R.id.edit);
        //获取读取的数据
        String inputText = load();
        if (!TextUtils.isEmpty(inputText)) {
            mEditText.setText(inputText);
            mEditText.setSelection(inputText.length());
            Toast.makeText(MainActivity.this, "成功存储", Toast.LENGTH_SHORT).show();
        }

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //获取了EditText中输入的内容
        String inputText = mEditText.getText().toString();
        //调用save()方法将输入的内容存储到文件中
        save(inputText);
    }

    public void save(String inputText) {
        FileOutputStream out = null;
        BufferedWriter writer = null;
        try {
            //获取节点流OutputStream
            out = openFileOutput("data", Context.MODE_PRIVATE);
            //通过包装流获得缓冲流
            writer = new BufferedWriter(new OutputStreamWriter(out));
            //缓冲流执行写入操作,将数据写入文件中
            writer.write(inputText);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null) {
                    //缓冲流不要忘记关闭
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();

            }
        }

    }

    public String load() {
        FileInputStream inputStream = null;
        BufferedReader reader = null;
        //用于存放缓冲流读取出来的数据
        StringBuilder content = new StringBuilder();

        try {
            //用于从文件中读取数据
            inputStream = openFileInput("data");
            //由节点流获取包装流再由包装流获取缓冲流
            reader = new BufferedReader(new InputStreamReader(inputStream));
            String line = "";
            //通过缓冲流进行一行行读取
            while ((line = reader.readLine()) != null) {
                content.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //最后将读取到的内容返回即可
        return content.toString();

    }
}

SharedPreferences存储

SharedPreferences是使用键值对的方式来存储数据的,在存储数据时,需要给这条数据指定一个对应的键,这样在读取数据的时候便可以通过这个键把相应的值取出来.SharedPreferences存储还支持不同的数据类型存储
SharedPreferences应用很广泛,比如我们所熟知的记住密码,还有很多的偏好设置都用到了SharedPreferences技术

将数据存储到SharedPreferences中(写入数据)

* 1获取SharedPreferences对象(三种方法推介使用最后一种因为不论在什么情况下都能使用)
     Context类中的getSharedPreferences()方法
     Activity类中的getPreferences()方法
     PreferenceManager类中的getDefaultSharedPreferences()方法
* 2获取SharedPreferences.Editor对象
* 3向SharedPreferences.Editor对象中添加数据
* 4调用apply()方法将添加的数据提交,完成数据存储操作 
三种方法详解

从SharedPreferences中读取数据

SharedPreferences对象中提供了一系列的get方法,用于对存储的数据进行读取,每种get()方法都对应了SharedPreferences.Edior中的put()方法.方法接收两个参数,第一个是键值,第二个参数是默认值,表示传入键找不到对应的值时会以什么样的默认值进行返回.

public class MainActivity extends AppCompatActivity {
    private Button mBtnSavedata;
    private Button mBtnRestoredata;
    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtnSavedata=findViewById(R.id.save_data);
        mBtnRestoredata=findViewById(R.id.restore_data);
        mBtnSavedata.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //调用Context的getSharedPreferences()方法来获取SharedPreferences对象,
                //方法中第一个参数指定了SharedPreferences的文件名,第二个参数只有这一种模式可以选其余的都废弃掉了
                SharedPreferences sharedPreferences= getSharedPreferences("data",MODE_PRIVATE);
                //获取SharedPreferences.Editor
                SharedPreferences.Editor editor=sharedPreferences.edit();
                //利用SharedPreferences.Editor的putXXX方法写入XXX型的数据,方法中第一个参数是键名,第二个参数是具体的数据
                editor.putString("name","owen");
                editor.putInt("age",25);
                editor.putBoolean("married",false);
                //调用editor的apply()方法进行数据的提交
                editor.apply();
            }
        });
        mBtnRestoredata.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //调用Context的getSharedPreferences()方法来获取SharedPreferences对象,
                //方法中第一个参数指定了SharedPreferences的文件名,第二个参数只有这一种模式可以选其余的都废弃掉了
               SharedPreferences sharedPreferences =getSharedPreferences("data",MODE_PRIVATE);
                //调用SharedPreferences的getXXX()方法来获取XXX型的数据,该方法需要指定两个参数
                //第一个参数是需要获取的数据所对应的键名,第二个参数是如果传入的键找不到对应的值的时候会返回什么样的默认值
                //即如果没有找到相应的值,就会使用方法中传入的默认值来代替
               String name=sharedPreferences.getString("name","");
               int age=sharedPreferences.getInt("age",0);
               boolean married=sharedPreferences.getBoolean("married",false);
                Log.d("MainActivity","name is"+name);
                Log.d("MainActivity","age is"+age);
                Log.d("MainActivity","married is"+married);
            }
        });
    }
}

我们之前写的记住密码的部分代码

public class LoginActivity extends AppCompatActivity {
    private Button mBtnLogin;
    private EditText mETAccount;
    private  EditText mETPassword;
    private CheckBox mCBRememberPass;
    private SharedPreferences sharedPreferences;
    private  SharedPreferences.Editor editor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        mBtnLogin=findViewById(R.id.login);
        mETAccount=findViewById(R.id.account);
        mETPassword=findViewById(R.id.password);
        mCBRememberPass=findViewById(R.id.remember_pass);
        //通过PreferceManager.getDefaultSharedPreferences()方法来获取SharedPreferences对象也是我们所提倡的方法
        sharedPreferences= PreferenceManager.getDefaultSharedPreferences(this);
        //获取SharedPreferences.Editor对象
        editor=sharedPreferences.edit();
        boolean isRemeber=sharedPreferences.getBoolean("remember_password",false);
        if (isRemeber){
            //将账号的密码都设置到文本框中
            String account=sharedPreferences.getString("account","");
            String password=sharedPreferences.getString("password","");
            Log.d("LoginActivity","account is "+account);
            Log.d("LoginActivity","password is "+password);
            mETAccount.setText(account);
            mETPassword.setText(password);
            mCBRememberPass.setChecked(true);
        }

        mBtnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String account=mETAccount.getText().toString();
                String password=mETPassword.getText().toString();
                //如果账号是王茹雪且密码是19950115则认为登录成功
                if (account.equals("wangruxue")&&password.equals("19950115")){
                    if (mCBRememberPass.isChecked()){
                        //检查复选框是否被选中
                        //调用SharedPreferences.Editor的putXXX()方法,写入XXX型数据到SharedPreferences
                        editor.putBoolean("remember_password",true);
                        editor.putString("account",account);
                        editor.putString("password",password);

                    }else {
                        //将SharedPreferences文件中的数据清理掉
                        editor.clear();
                    }
                        //将添加的数据提交到SharedPreferences,完成数据存储工作
                        editor.apply();
                    Intent intent=new Intent(LoginActivity.this, UIActivity.class);
                    startActivity(intent);
                    finish();
                }else{
                    Toast.makeText(LoginActivity.this, "账号或者密码错误!!", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

SQLite数据库存储

支持标准的SQL语法,还遵循数据库的ACID事务

LitePal

LitePal是一款开源的Android数据库框架,采用ORM(对象关系映射)的模式,将我们平时开发常用的一些数据库功能进行了封装,使我们不用编写一行SQL语句就可以完成各种建表和增删改查的操作.

LitePal三部曲

配置LitePal

  • 1.导入闭包
compile 'org.litepal.android:core:1.6.1'
  • 2.右击app/src/main目录----->New---->Directory,创建一个assets目录,然后在assets目录下再新建一个litepal.xml文件,接着编辑litepal.xml文件中的内容.
<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="BookStore" />
    <version value="1" />
    <list>
       
    </list>
</litepal>

<dbname>标签用于指定数据库名,<version>标签用于指定数据库版本号,<list>标签用于指定所有的映射模型.
最后再配置一下LitePalApplication修改AndroidManifest.xml中的代码,配置这个之后,LitePal就能在内部自动获取到Context了,在高阶技巧那个地方我们也已经讲过了.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.crazyit.litepal">
    <!--android:name="org.litepal.LitePalApplication"-->
    <application
        android:name="org.crazyit.litepal.Myapplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
public class Myapplication extends Application {
    public static Context mContext;
    @Override
    public void onCreate() {
        super.onCreate();
        mContext = getApplicationContext();
        //LitePal的处理办法
       LitePal.initialize(mContext);
    }
    public static Context getmContext(){
        return mContext;
    }
}

创建和升级数据库

LitePal采取的是对象关系映射(ORM)的模式,简单来说,就是我们使用的语言是面向对象的语言,而使用的数据库是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,就是对象关系映射.用面向对象的思维来操纵数据库.

  • 1.为了以后的增删改查(CURD)方便我们直接定义一个类来继承DataSupport
public class Book extends DataSupport{
    private String author;
    private  int id;
    private  int pages;
    private  double prices;
    private  String name;
    //想要向Book表中新增一个press(出版社)的列,只需在相关类中添加一个字段即可 alt+insert getter and setter
    private  String press;
    public String getPress() {
        return press;
    }
    public void setPress(String press) {
        this.press = press;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getPages() {
        return pages;
    }
    public void setPages(int pages) {
        this.pages = pages;
    }
    public double getPrices() {
        return prices;
    }
    public void setPrices(double prices) {
        this.prices = prices;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
  • 2.将我们创建的这个类添加到映射模型列表中,修改litepal.xml中代码:注意这个地方一定要使用完整的类名(即这个类在AS中的准确位置,在哪些文件夹下),不管有多少模型需要映射,都使用同样的方式配置在<list>标签下即可.
<list>
        <mapping class="org.crazyit.litepal.Book" ></mapping>
    </list>
  • 3.然后在代码中调用LitePal.getDatabase()方法创建数据库.
LitePal.getDatabase();

我们可以看到在数据库BookStore中出现了一个book表(注意数据库中生成的这个表都是小写的book要额外注意这一点,我们定义的表类的首字母是大写的Book),这个book表就是我们定义的Book类以及类中的字段来自动生成的

升级数据库

如果我们想在Book表中添加一个press列,直接修改Book类的代码,然后给它添加一个字段即可

public class Book extends DataSupport{
    ......
    //想要向Book表中新增一个press(出版社)的列,只需在相关类中添加一个字段即可 alt+insert getter and setter
    private  String press;

    public String getPress() {
        return press;
    }
    public void setPress(String press) {
        this.press = press;
    }
......
}

如果我们还想在添加一张新表---Category表,那么只需要新建一个Category类就可以了.

public class Category {
    private  String categoryName;
    private  int categoryCode;
    private  int id;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getCategoryName() {
        return categoryName;
    }
    public void setCategoryName(String categoryName) {
        this.categoryName = categoryName;
    }
    public int getCategoryCode() {
        return categoryCode;
    }
    public void setCategoryCode(int categoryCode) {
        this.categoryCode = categoryCode;
    }
}

改完了我们想改的东西,然后记得在litepal.xml中将版本号+1就行了,然后如果新添加了一个新的模型类(比如这里的Category类),需要将它添加到映射模型列表中.

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="BookStore" />
    <version value="2" />
    <list>
        <mapping class="org.crazyit.litepal.Book" ></mapping>
        <mapping class="org.crazyit.litepal.Category" ></mapping>
    </list>
</litepal>

可以看到我们新建的数据库BookStore中的book表下面新增了一个press列,同时BookStore中新增了一个category表

增删改查

添加数据

使用LitePal添加数据

  • 1.首先需要我们创建出模型类的实例
  • 2.然后将要存储的数据设置好
  • 3.最后调用一下save()方法即可

下面截取LitePal中添加数据的部分代码

mBtnadd=findViewById(R.id.add_database);
        mBtnadd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //创建模型类的实例(模型类其实就是数据库中的表)
                Book book=new Book();
                //将要存储的数据设置好
                //设置数据采用setXXX方法
                book.setAuthor("xu dong lei");
//                book.setId();
                book.setName("san guo");
                book.setPages(454);
                book.setPrices(16.96);
                book.setPress("bu zhi dao");
                //最后记得保存
                //这个save()方法其实是从DataSupport中继承而来的
                book.save();
                Toast.makeText(MainActivity.this,"success",Toast.LENGTH_SHORT).show();
            }
        });
使用LitePal更新数据

更新数据部分代码如下:

  • 1对已存储的对象进行操作更新数据------限制性比较大
mBtnupdate=findViewById(R.id.update_database);
        mBtnupdate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //前面的套路和添加数据基本相同,不做过多的阐释
                Book book=new Book();
                book.setAuthor("xu dong lei");
                book.setName("yue fei zhuan");
                book.setPages(510);
                book.setPrices(19.95);
                book.setPress("bu zhi dao");
                book.save();
                //在已经调用过save方法的基础上再用set方法来设置数据达到更新数据的目的!
                book.setPrices(10.99);
                book.save();
            }
        });
  • 1.对已存储的对象重新设值,
  • 2.然后重新调用save()方法即可.

已存储对象:对于LitePal来说,对象是否已存储是根据调用model.isSaved()方法的结果判断的,返回true表示已经存储,返回false表示未存储.下面两种情况都会返回true,第一种情况是已经调用过model.save()方法去添加数据了,此时model会被认为是已存储对象;另一种情况是model对象通过LitePal提供的查询API(可以认为是查询方法)查出来的,由于是从数据库中查到的对象,因此也会被认为是已存储对象.

官方解释:API(Application Programming Interface,应用程序编程接口)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制

  • 2采用updateAll()方法灵活更新
mBtnupdata1=findViewById(R.id.update_database1);
        mBtnupdata1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //首先new出一个Book实例
                Book book=new Book();
                //直接调用setXXX()方法来设置要更新的数据
                book.setPrices(14.95);
                book.setPress("Anchor");
                //最后调用updateAll()方法去执行更新操作
                //采用updateAll这种方法即使没有已存储对象也能更新数据
                //updateAll()方法中可以指定一个条件约束,如果不指定条件约束的话默认更新所有数据
                //注意这个地方以前存的时候徐冬磊是什么样就是什么样 不要给它更改样式不然找不到 以前是 xu dong lei
                book.updateAll("name=? and author=?","san guo","xu dong lei");
            }
        });
  • 1.首先new出一个Book实例
  • 2.直接调用setXXX()方法来设置要更新的数据
  • 3.最后调用updateAll()方法去执行更新操作

updateAll()方法需要注意的一个点:
如果想要将数据更新为默认值的操作,LitePal提供了一种setToDefault()方法,直接传入相应的列名即可实现了.(因为当new出一个新对象的时候其实所有的字段都已经被初始化为默认值了,所以如果你还通过set方法来设置更新数据为默认值的话,系统是不会给我们更新的)

使用LitePal删除数据

LitePal删除数据方式主要有两种:

  • 1直接调用已存储对象的delete()方法即可
//LitePal的第一种删除数据的方法
        mBtnDeleteData=findViewById(R.id.delete_database);
        mBtnDeleteData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book=new Book();
                book.setAuthor("wang ru xue");
                book.setName("jin ping mei");
                book.setPages(510);
                book.setPrices(19.95);
                book.setPress("bu zhi dao");
                book.save();
                //可以通过打开adb shell 来看其数据id来知道这个方法是新建一条数据马上又把这条数据删除  所以
                // 当你一直电机这个按钮然后再注释掉book.delete();这句话看到的数据的id不是一些连续的值
                book.delete();
            }
        });
  • 2调用DataSupport.deleteAll()方法删除数据
mBtndelecte=findViewById(R.id.delecte_database);
        mBtndelecte.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //deleteAll()方法的第一个参数用于指定删除哪张表的数据Book.class意味着删除Book表中的数据后面的参数用于指定约束条件
                //deleteAll()方法如果不指定约束条件,意味着你要删除表中的所有的数据
                //删除Book表中的所有的prices 大于12的数据  注意这里的prices跟之前定义的要一致,不能之前写的是prices这里写price这样就大错特错了!
                DataSupport.deleteAll(Book.class,"prices>?","12");
            }
        });
使用LitePal查询数据

LitePal在查询API方面的设计极为人性化,代码极为简洁.
核心是调用DataSupport的findAll()方法,该方法可以通过连缀很多查询方法来丰富查询的功能
调用DataSupport的findAll()方法,该方法返回值是一个Book类型的List集合.不用像之前使用SQLDatebase一样通过Cursor对象一行行去取值了.LitePal已经自动帮我们完成了赋值操作.
之前我们使用query()方法,方法参数很冗长

Cursor cursor=db.query("Book",null,null,null,null,null,null);

现在我们用这一行代码便可解决

//DataSupport.findAll()方法返回的是一个泛型对象!!
 List<Book> books = DataSupport.findAll(Book.class);

DataSupport.findAll()方法源码

/**
     * Finds multiple records by an id array.
     * 
     * <pre>
     * List&lt;Person&gt; people = DataSupport.findAll(Person.class, 1, 2, 3);
     * 
     * long[] bookIds = { 10, 18 };
     * List&lt;Book&gt; books = DataSupport.findAll(Book.class, bookIds);
     * </pre>
     * 
     * Of course you can find all records by passing nothing to the ids
     * parameter.
     * 
     * <pre>
     * List&lt;Book&gt; allBooks = DataSupport.findAll(Book.class);
     * </pre>
     * 
     * Note that the associated models won't be loaded by default considering
     * the efficiency, but you can do that by using
     * {@link org.litepal.crud.DataSupport#findAll(Class, boolean, long...)}.
     * 
     * The modelClass determines which table to query and the object type to
     * return.
     * 
     * @param modelClass
     *            Which table to query and the object type to return as a list.
     * @param ids
     *            Which records to query. Or do not pass it to find all records.
     * @return An object list with found data from database, or an empty list.
     */
    public static synchronized <T> List<T> findAll(Class<T> modelClass, long... ids) {
        return findAll(modelClass, false, ids);
    }

通过连缀查询来定制更多查询功能

                //select()方法用于指定查询哪几列的数据
                List<Book> books1 = DataSupport.select("name", "author").find(Book.class);
                //where()方法用于指定查询的约束条件,比如只查询页数>400的数据
                List<Book> books2 = DataSupport.where("pages>?").find(Book.class);
                //order()方法用于指定结果的排序方式,其中desc表示降序排列,asc或者不写表示升序排列
                List<Book> books3 = DataSupport.order("price desc").find(Book.class);
                //limit()方法用于指定查询结果的数量,比如只查询表中的前三条数据
                List<Book> books4 = DataSupport.limit(3).find(Book.class);
                //offset()方法用于指定查询结果的偏移量;比如查询表中第二条第三条第四条数据可以这样写
                List<Book> books5 = DataSupport.limit(3).offset(1).find(Book.class);
                //将五种方法进行任意连缀组合,完成一个比较复杂的查询操作
                List<Book> books6 = DataSupport.select("name", "author", "pages")
                        .where("pages>?", "400")
                        .order("pages")
                        .limit(10)
                        .offset(10)
                        .find(Book.class);

内容提供器

数据共享的标准:ContentProvider

ContentProvider是不同应用程序之间进行数据交换的标准API.ContentProvider以某种Uri的形式对外提供数据,允许其他应用访问或修改数据;其他应用程序使用ContentResolver根据Uri去访问操作指定的数据.

把ContentProvider当成Android系统内部的"网站",这个网站以固定的Uri对外提供服务;
把ContentResolver当成Android系统内部的HttpClient,它可以向指定Uri发送"请求",这种请求最终委托给ContentProvider处理,从而实现对"网站"(即ContentProvider)内部数据进行操作

内容提供器-------ContentProvider

  • 1定义自己的ContentProvider类(我自己定义的为DatabaseProvider),该类继承Android提供的ContentProvider基类
  • 2向Android系统注册这个"网站"----即在AndroidManifest.xml文件中注册这个ContentProvider,就像注册Activity一样.注册ContentProvider需要为它绑定一个Uri即下面的android:authorities属性
        <!--下面配置中name属性指定ContentProvider类-->
        <!--authorities就相当于为该ContentProvider指定域名-->
        <provider android:name=".DatabaseProvider"
            android:authorities="org.crazyit.databasetest.provider"
            android:enabled="true"
            android:exported="true">
  • 3当我们通过上面配置文件注册了DatabaseProvider之后,其他应用程序就可通过该Uri来访问DatabaseProvider所暴露的数据了.
    DatabaseProvider是如何暴露它所提供的的数据呢?其实很简单,应用程序对数据的操作无非就是CRUD操作,因此DatabaseProvider除了需要继承ContentProvider之外,还需要提供如下几个方法:
public class DatabaseProvider extends ContentProvider {
//根据Uri删除selection条件所匹配的全部记录
public int delete(Uri uri, String selection, String[] selectionArgs)
//该方法用于返回当前Uri所代表的数据的MIME类型
//如果该Uri对应的数据可能包含多条记录,那么MIME类型字符串应该以vnd.android.cursor.dir/开头
//如果该Uri对应的数据可能只包含一条记录,那么MIME类型字符串应该以vnd.android.cursor.item/开头
public String getType(Uri uri)
//根据该Uri插入values对应的数据
public Uri insert(Uri uri, ContentValues values)
//该方法在ContentProvider创建后会被调用,当其他应用程序第一次访问ContentProvider时,该ContentProvider会被创建出来,并立刻回调该onCreate()方法
public boolean onCreate()
//根据Uri查询出selection所匹配的全部记录,其中projection就是一个列名表,表明只选择出指定的数据列
public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder)
//根据Uri更新selection条件所匹配的全部记录
public int update(Uri uri, ContentValues values, String selection,String[] selectionArgs)
}

Uri和URL的对比

我们之前说Uri是Android系统内部的"网站",URL是统一资源定位符;
URL官方解释:统一资源定位符是对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。

URL:统一资源定位符,就是一个网址:协议名://域名:端口/路径,例如:http://www.oldboy.cn:80/index.html

我们先来看一个最常见的互联网的URL:

http://www.crazyit.org:80/ethos.php
对于上面的URL我们可以将其分为3部分
* 1.http://------URL协议部分,只要通过Http协议来访问网站,这个部分就是固定的
* 2.www.crazyit.org------域名部分,只要访问指定的网站,这个部分就是固定的
* 3.ethos.php-----网站资源部分.当访问者需要访问不同资源时,这个部分是动态改变的

我们再来看一下ContentProvider要求的Uri

content://org.crazyit.providers.dictprovider/words
它也可以分为3部分:
* 1.content://-----这个部分是Android的ContentProvider规定的,就像上网的协议默认是http://一样,暴露ContentProvider,访问ContentProvider的默认协议就是content://
* 2.org.crazyit.providers.dictprovider---这个部分是ContentProvider的authorities.**系统就是通过这个部分来找到操作哪个ContentProvider的**,只要访问指定的ContentProvider,这个部分就是固定的.
* 3.words:资源部分(或者说数据部分)当访问者需要访问不同的资源的时候,这个部分是动态改变的.

Android的Uri所表达的功能更丰富

//访问word数据中ID为2的记录
content://org.crazyit.providers.dictprovider/word/2
//访问word数据中ID为2的记录的word字段
content://org.crazyit.providers.dictprovider/word/2/word
//访问全部数据
content://org.crazyit.providers.dictprovider/words

大部分使用ContentProvider所操作的数据都来自于数据库,但有时候这些数据也可以来自于文件,XML或网络或其他存储方式,

//该Uri表示操作word节点下的detail节点
content://org.crazyit.providers.dictprovider/word/detail/

既然Uri这么重要,Uri工具类提供了parse()静态方法用来将一个字符串转换为Uri

//将字符串content://org.crazyit.providers.dictprovider/word/2转换为Uri
Uri uri=Uri.parse("content://org.crazyit.providers.dictprovider/word/2")

使用ConteResolver操作数据

前面已经提到,ContentProvider相当于一个"网站",它的作用是暴露可操作的数据;其他应用程序则可通过ContentResolver来操作ContentProvider所暴露的数据,ContentResolver相当于HttpClient https://baike.baidu.com/item/httpclient.

使用ConteResolver操作数据步骤:

  • 1.获取该应用默认的ContentResolver-----Context提供的getContentResolver()方法
  • 2调用ContentResolver的CURD方法来操作数据

ContentResolver的CURD方法的部分代码如下:

//根据Uri删除where条件所匹配的全部记录
public int delete(Uri uri, String where, String[] selectionArgs)
//根据该Uri插入values对应的数据
public Uri insert(Uri uri, ContentValues values)
//查询Uri所对应的ContentProvider中where提交匹配的数据
public Cursor query(Uri uri, String[] projection, String where,String[] selectionArgs, String sortOrder)
//更新Uri对应的ContentProvider中where提交匹配的数据
public int update(Uri uri, ContentValues values, String where,String[] selectionArgs)

ContentProvider是单实例模式的,当多个应用通过ContentResolver来操作ContentProvider所提供的数据时,ContentResolver调用的数据操作将会委托给同一个ContentProvider来处理

ContentProvider与ContentResolver的关系

无论是ContentProvider还是ContentResolver,他们提供的CURD方法的第一个参数都是Uri,也就是说Uri是ContentProvider和ContentResolver进行数据交换的标识,ContentResolver对指定的Uri执行CRUD等数据操作,但Uri并不是真正的数据中心,因为这些CURD操作会委托给该Uri所对应的ContentProvider来实现.

举例来说:假如A应用通过ContentResolver执行CURD操作,这些CRUD操作都需要指定Uri参数,Android系统就根据该Uri找到对应的ContentProvider(该ContentProvider通常属于B应用),ContentProvider则负责实现CRUD方法,完成对底层数据的增,删,改,查等操作,这样就可以让A应用访问,修改B应用的数据了.


ContentResolver与Uri与ContentProvider之间的关系

2

开发ContentProvider子类

  • 1定义自己的ContentProvider子类,该子类需要实现query insert update delete方法
  • 2在androidManifest.xml文件中注册该ContentProvider,指定android:authorities属性

ContentProvider子类中的query insert update delete方法并不是给该应用本身调用的,而是供其他应用调用的.如何实现ContentProvider的query insert update delete方法完全由程序员决定,你想怎么暴露该应用数据,就怎么实现这4个方法.

  • 3.在该ContentProvider供其他应用调用之前,还需要先配置该ContentProvider

Android应用要求所有应用程序组件(Activity Service ContentProvider BroadcastReceiver)都必须显式配置
配置片段代码如下:

<!--name:指定该ContentProvider的实现类的类名-->
        <!--authorities:指定该ContentProvider对应的Uri(相当于为该ContentProvider分配一个域名)-->
        <!--android:exported:指定该ContentProvider是否允许其他应用程序调用-->
        <provider android:name=".DatabaseProvider"
            android:authorities="org.crazyit.databasetest.provider"
            android:enabled="true"
            android:exported="true">
            </provider>
  name:指定该ContentProvider的实现类的类名
  authorities:指定该ContentProvider对应的Uri(相当于为该ContentProvider分配一个域名)
  android:exported:指定该ContentProvider是否允许其他应用程序调用

上面的配置意味着该ContentProvider被绑定到"content://org.crazyit.databasetest.provider"-----意味着当其它应用的ContentResolver向该Uri执行query insert update delete方法时,实际上是调用该ContentProvider的query insert update delete方法

ContentResolver调用方法的参数将会传给该ContentProvider的query(),insert(),update()和delete()方法

ContentResolver调用方法的返回值,也就是ContentProvider执行query(),insert(),update()和delete()方法的返回值

使用ContentResolver调用方法

Context提供了getContentResolver()方法,这表明Activity,Service等组件都可以通过getContentResolver()方法获取到ContentResolver对象

获取了ContentResolver对象之后,接下来就可调用ContentResolver的query(),insert(),update()和delete()方法------实际上是指定Uri对应的ContentProvider的query(),insert(),update()和delete()方法

public class MainActivity extends AppCompatActivity {
    private Button addData,queryData,updateData,deleteData;
    private String newId;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        addData=findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //添加数据
                //这里的Book在第一行代码P212中可以看出Book指的是BookStore数据库下的Book表,即表示对Book表进行更新
                Uri uri=Uri.parse("content://org.crazyit.databasetest.provider/Book");
                ContentValues values=new ContentValues();
                values.put("name","A Clash of Kings");
                values.put("author","George Martin");
                values.put("pages",1040);
                values.put("price",22.85);
                values.put("price",55.55);
                values.put("pages",2040);
                //首先通过Context的getContentResolver()方法来获取ContentResolver
                //调用ContentResolver的insert()方法
                //实际上返回的Uri是该Uri所对应的ContentProvider的query()的返回值
                Uri newUri=getContentResolver().insert(uri,values);
                newId=newUri.getPathSegments().get(1);
                Toast.makeText(MainActivity.this,"远程ContentProvider新插入的记录的Uri为:"+newUri,Toast.LENGTH_SHORT).show();
            }
        });
        queryData=findViewById(R.id.query_data);
        queryData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //查询数据
                Uri uri=Uri.parse("content://org.crazyit.databasetest.provider/Book");
                //首先通过Context的getContentResolver()方法来获取ContentResolver
                //调用ContentResolver的query()方法
                //实际上返回的cursor是该Uri所对应的ContentProvider的query()的返回值
                Cursor cursor=getContentResolver().query(uri,null,null,null,null);
                Toast.makeText(MainActivity.this,"远程ContentProvider返回的Cursor为:"+cursor,Toast.LENGTH_SHORT).show();
                if (cursor!=null){
                    while(cursor.moveToNext()){
                        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"));
                        Log.d("MainActivity","book name is"+name);
                        Log.d("MainActivity","book author is"+author);
                        Log.d("MainActivity","book pages is"+pages);
                        Log.d("MainActivity","book price is"+price);
                    }
                    cursor.close();
                }
            }
        });
        updateData=findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //更新数据
                Uri uri=Uri.parse("content://org.crazyit.databasetest.provider/Book/"+newId);
                ContentValues values=new ContentValues();
                values.put("name","A Storm of Swords");
                values.put("pages",1216);
                values.put("price",24.05);
                //首先通过Context的getContentResolver()方法来获取ContentResolver
                //调用ContentResolver的update()方法
                //实际上返回的int型数据count是该Uri所对应的ContentProvider的query()的返回值
                int count=getContentResolver().update(uri,values,null,null);
                Toast.makeText(MainActivity.this,"远程ContentProvider新更新记录数为:"+count,Toast.LENGTH_SHORT).show();
            }
        });
        deleteData=findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //删除数据
                Uri uri=Uri.parse("content://org.crazyit.databasetest.provider/Book/"+newId);
                //首先通过Context的getContentResolver()方法来获取ContentResolver
                //调用ContentResolver的query()方法
                //实际上返回的int型数据count是该Uri所对应的ContentProvider的query()的返回值
                int count=getContentResolver().delete(uri,null,null);
                Toast.makeText(MainActivity.this,"远程ContentProvider新删除的记录的Uri为:"+count,Toast.LENGTH_SHORT).show();
            }
        });
    }
}
<provider
            android:name="org.crazyit.databasetest.DatabaseProvider"
            android:authorities="org.crazyit.databasetest.provider"
            android:enabled="true"
            android:exported="true"></provider>
    </application>

可以看出来当Android应用通过ContentProvider调用query(),insert(),update()和delete()方法时,实际上是调用的DatabaseProvider的 query(),insert(),update()和delete()方法-------(该DatabaseProvider的Uri为:content://org.crazyit.databasetest.provider)

创建ContentProvider的说明

ContentProvider不像Activity存在复杂的生命周期,ContentProvider只有一个onCreate()生命周期方法------当其他应用通过ContentResolver第一次访问该ContentProvider时,onCreate()方法会被回调,onCreate()方法只会被调用一次;ContentProvider提供的query(),insert(),update()和delete()方法则由其他应用通过ContentResolver调用.

开发ContentProvider时所实现的query(),insert(),update()和delete()方法的第一个参数为Uri,该参数由ContentResolver调用这些方法时传入.

为了确定该ContentProvider实际能处理Uri,以及确定每个方法中Uri参数所操作的数据,Android系统提供了UriMatcher工具类.

UriMatcher工具类主要提供下面两个方法:

//该方法用于向UriMatcher对象注册Uri.其中authority和path组合成一个Uri,而code则代表该Uri对应的标识码
void add(String authority,String path, int code)
//根据前面注册的Uri来判断指定Uri对应的标识码.如果找不到匹配的标识码,该方法将会返回-1

例如我们通过如下代码来创建UriMatcher对象

UriMatcher matcher=new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI("org.crazyit.providers.dictprovider","words",1);
matcher.addURI("org.crazyit.providers.dictprovider","word/#",2);

上面创建的UriMatcher对象注册了两个Uri,其中
org.crazyit.providers.dictprovider/words对应的标识码是1;
org.crazyit.providers.dictprovider/word/#对应的标识码为2,其中#为通配符
这就意味着如下匹配结果:

//返回标识码1
matcher.match(Uri.parse("content://org.crazyit.providers.dictprovider/words"));
//返回标识码2,因为#是通配符
matcher.match(Uri.parse("content://org.crazyit.providers.dictprovider/word/#"));
//返回标识码2,因为#是通配符
matcher.match(Uri.parse("content://org.crazyit.providers.dictprovider/word/2"));
//返回标识码2,因为#是通配符
matcher.match(Uri.parse("content://org.crazyit.providers.dictprovider/word/10"));

对于content://org.crazyit.providers.dictprovider/words这个Uri,它的资源部分是words,这种资源代表了访问所有数据项
对于content://org.crazyit.providers.dictprovider/word/2这个Uri,它的资源部分通常代表访问指定数据项,其中最后一个数字代表了该数据的ID

操作系统的ContentProvider

Android系统本身提供了大量的ContentProvider,例如联系人信息,系统的多媒体信息等等.开发者开发自己的ContentResolver来调用系统的ContentProvider提供的query(),insert(),update()和delete()方法,这样开发者即可通过这些ContentProvider来获取Android内部的数据了.

使用ContentResolver操作系统ContentProvider数据的步骤

  • 1调用Context的getContentResolver()获取ContentResolver对象
  • 2根据需要调用ContentResolver的query(),insert(),update()和delete()方法操作数据.

为了操作系统提供的ContentProvider,需要了解该ContentProvider的Uri,以及该ContentProvider所操作的数据列的列名,可以通过查询Android官方文档来获取这些信息
读取系统联系人的代码

public class MainActivity extends AppCompatActivity {
    private ListView mLvContactsView;
    ArrayAdapter<String> adapter;
    List<String> contactsList=new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mLvContactsView=findViewById(R.id.contacts_view);
        adapter=new ArrayAdapter<>(MainActivity.this,android.R.layout.simple_list_item_1,contactsList);
        mLvContactsView.setAdapter(adapter);
        //检查该应用程序是否已经获得危险权限的使用权如果没有则需要申请权限
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED
                ){
            //调用ActivityCompat的requestPermissions方法来申请权限
            ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.READ_CONTACTS},1);
        }else{
            //新建一个 readContacts()方法
            readContacts();
        }
    }
    private void readContacts() {
        Cursor cursor=null;
        try{
            //首先通过Context的getContentResolver()方法来获取ContentResolver
            //调用ContentResolver的query()方法
            //实际上返回的是该Uri所对应的ContentProvider的query()的返回值
            cursor=getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
            if (cursor!=null){
                while(cursor.moveToNext()){
                    //获取联系人姓名
                    String displayName =cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                    //获取联系人手机号
                    String number =cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                    contactsList.add(displayName+"\n"+number);
                }
                adapter.notifyDataSetChanged();
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (cursor!=null){
                cursor.close();
            }
        }
    }
    //通过请求权限结果回调的方式判断是否用户已经授予我们权限,授予的结果保存在grantResults[]数组中
    //我们可以通过requestCode来对不同的申请权限条目进行判断,因为不同的权限我们可以给它设置不同的requestCode
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                if (grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
                    readContacts();
                }else {
                    Toast.makeText(this,"你没有权限",Toast.LENGTH_SHORT).show();
                }
                break;
                default:
        }
    }
}

使用ContentProvider管理多媒体内容

调用手机摄像头拍照

public class MainActivity extends AppCompatActivity {
    private Button mBtntakephoto;
    private Button mBtnchoosephoto;
    private ImageView mTvpicture;
    private Uri imageUri;
    public static final int TAKE_PHOTO = 1;
    public static final int CHOOSE_PHOTO = 2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtntakephoto = findViewById(R.id.btn_photo);
        mTvpicture = findViewById(R.id.iv_picture);
        mBtntakephoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //创建File对象,用于存储拍照的图片,并将其存放在手机SD卡的应用关联缓存目录下
                //应用关联存储目录就是SD卡中专门用于存放当前应用缓存数据的位置调用getExternalCacheDir()
                // 就可以得到这个目录,具体路径是/sdcard/Android/data/<package name>/cache
                //因为从Android6.0开始读写SD卡列为了危险权限,如果将图片保存到SD卡的任何其它目录,
                // 都需要进行运行权限处理才行,而使用应用关联目录则可以跳过这一步
                File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
                try {
                    if (outputImage.exists()) {
                        outputImage.delete();
                    }
                    outputImage.createNewFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (Build.VERSION.SDK_INT >= 24) {
                    //调用FileProvider的getUriForFile()方法将File对象转换成一个封装过的Uri对象
                    //从Android7.0系统开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出一个FileUriExposedException异常
                    //FileProvider是一种特殊的内容提供器,它使用和内容提供器类似的机制来对数据进行保护,可以选择性的将封装过Uri分享给外部,从而提高了应用的安全性
                    imageUri = FileProvider.getUriForFile(MainActivity.this, "org.crazyit.cameraalbumtest.fileprovider", outputImage);
                } else {
                    //如果运行设备的系统版本低于Android7.0,就调用Uri的fromFile()方法将File对象转换成Uri对象,
                    //这个Uri对象标识着output_image.jpg这张照片的真实路径
                    imageUri = Uri.fromFile(outputImage);
                }
                //启动照相机程序
                Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
                //调用Intent的putExtra()方法指定图片的输出地址
                intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                startActivityForResult(intent, TAKE_PHOTO);
            }
        });
        mBtnchoosephoto = findViewById(R.id.btn_choose_from_album);
        mBtnchoosephoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //运行时的权限处理----操作手机存储器需要进行危险权限处理
                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
                } else {
                    //打开相册
                    openAlbum();

                }
            }
        });
    }

    private void openAlbum() {
        Intent intent = new Intent("android.intent.action.GET_CONTENT");
        intent.setType("image/*");
        //打开相册,第二个参数选择的是CHOOSE_PHOTO这样当相册选择完照片回到onActivityResult()方法时
        //就会进入CHOOSE_PHOTO的case来处理照片
        startActivityForResult(intent, CHOOSE_PHOTO);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    openAlbum();
                } else {
                    Toast.makeText(this, "你没有权限", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                break;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case TAKE_PHOTO:
                if (resultCode == RESULT_OK) {
                    Bitmap bitmap = null;
                    try {
                        //将拍摄的照片显示出来
                        //调用BitmapFactory的decodeStream()方法将output_image.jpg这张照片解析成Bitmap对象,然后将其设置到ImageView中显示出来
                        bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                        mTvpicture.setImageBitmap(bitmap);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case CHOOSE_PHOTO:
                if (requestCode == CHOOSE_PHOTO) {
                    //判断手机系统版本号
                    if (Build.VERSION.SDK_INT >= 19) {
                        //4.4及以上系统使用这个方法处理图片
                        handleImageOnKitKat(data);
                    } else {   //4.4以下系统使用这个方法处理图片
                        handleImageBeforeKitKat(data);
                    }
                }
                break;
            default:
                break;
        }
    }

    private void handleImageOnKitKat(Intent data) {
        String imagePath = null;
        Uri uri = data.getData();
        if (DocumentsContract.isDocumentUri(this, uri)) {
            //如果是document类型的Uri,则通过document id处理
            String docId = DocumentsContract.getDocumentId(uri);
            //如果Uri的authority是media格式的话,documentid 还需要再进行一次解析
            if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
                //通过字符串分割的方式取出后半部分才能得到真正的数字id
                String id = docId.split(":")[1];
                //取出的数字id用于构建新的Uri和条件语句
                String selection = MediaStore.Images.Media._ID + "=" + id;
                //然后把这些值作为参数传入到displayImage()方法将图片显示到界面上
                imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
            } else if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
                imagePath = getImagePath(contentUri, null);
            }
        } else if ("content".equalsIgnoreCase(uri.getScheme())) {
            //
            imagePath = getImagePath(uri, null);
        } else if ("file".equalsIgnoreCase(uri.getScheme())) {
            imagePath = uri.getPath();
        }   //调用displayImage()方法将图片显示到界面上
        displayImage(imagePath);
    }

    private void handleImageBeforeKitKat(Intent data) {
        //它的Uri是没有进行过封装的,不需要解析
        Uri uri = data.getData();
        //直接将Uri传入到getImagePath()方法中就能获取到图片的真是路径了
        String imagePath = getImagePath(uri, null);
        //调用displayImage()方法将图片显示到界面上
        displayImage(imagePath);
    }

    private String getImagePath(Uri uri, String selection) {
        String path = null;
        //通过Uri和selection来获取真是的图片路径
        Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
        if (cursor != null) {
            if (cursor.moveToNext()) {
                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
            }
            cursor.close();
        }
        return path;
    }

    private void displayImage(String imagePath) {
        if (imagePath != null) {
            Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
            mTvpicture.setImageBitmap(bitmap);
        } else {
            Toast.makeText(MainActivity.this, "获取图片失败", Toast.LENGTH_SHORT).show();
        }
    }
}

上面代码中用到了内容提供器,所以需要我们在AndroidManifest.xml文件中对内容提供器进行注册

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="org.crazyit.cameraalbumtest.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true"
            >
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"
                />
        </provider>

对于上面代码的几点解释

android:name的属性值是固定的
android:authorities属性的值必须要和刚才FileProvider.getUriForFile()方法的第二个参数一致起来
<provider>标签的内部使用<meta-data>来指定Uri的共享路径,并引用一个@xml/file_paths资源 

我们新建一个file_paths.xml文件 ,右击res目录--->New--->Directory,创建一个xml目录,接着右击xml目录--->New--->File,创建一个file_paths文件

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name = "my_images" path="" />
    <!--external-path就是用来指定Uri共享的,name的值可以随便填,path属性的值表示共享的具体路径-->
    <!--设置空值就表示将整个SD卡进行共享,你也可以仅共享我们存放output_image.jpg这张图片的路径-->
</paths>

播放多媒体文件

音频一般是MediaPlayer视频一般是VideoView ---------本质上VideoView只是帮我们做好了一个封装而已,它的背后仍然是使用MediaPlayer来对视频文件进行控制的

播放音频四部曲
1.首先创建一个MediaPlayer对象
2.调用setDataSource()方法来设置音频文件的路径
3.调用prepare()方法让MediaPlayer进入到准备状态
4.调用start()方法就可以开始播放音频,调用pause()方法就会暂停播放,调用restart()方法就会停止播放
public class MediaPlayerActivity extends AppCompatActivity implements View.OnClickListener {
    private Button mBtnPlay;
    private Button mBtnStop;
    private Button mBtnPause;
    private MediaPlayer mediaPlayer=new MediaPlayer();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mediaplayer);
        mBtnPlay=findViewById(R.id.play);
        mBtnPause=findViewById(R.id.pause);
        mBtnStop=findViewById(R.id.stop);
        mBtnPlay.setOnClickListener(this);
        mBtnPause.setOnClickListener(this);
        mBtnStop.setOnClickListener(this);
        //申请权限等系列操作
        if (ContextCompat.checkSelfPermission(MediaPlayerActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
        {
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
        }else{
            //要执行的初始化Mediaplayer
            initMediaPlayer();
        }
    }
    private  void  initMediaPlayer(){
        try {
            //我们创建了一个File对象,用于存放音频文件,这个音频文件的名字叫music.mp3
           //通过创建File对象来指定音频文件的路径
           //需要事先在SD卡的根目录下放置一个名为music.mp3的音频文件
            File file=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),"music.mp3");
            //指定音频文件的路径
            mediaPlayer.setDataSource(file.getPath());
            Log.i("音乐文件路径", file.getPath());
            //让MediaPlayer进入到准备状态
            mediaPlayer.prepare();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                if (grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED)
                {
                    initMediaPlayer();
                }else{
                    Toast.makeText(MediaPlayerActivity.this,"拒绝了权限将无法使用本程序",Toast.LENGTH_SHORT).show();
                    finish();
                }
                break;
                default:
                    break;
        }
    }
    @Override
    public void onClick(View v) {
        switch (v.getId())
        {
            case R.id.play:
                //原来是这个地方出了问题  草草草草草以前是mediaPlayer.isPlaying()因为我们刚开始没有点play所以应该是不播放的!!
                //判断当前mediaPlayer是否正在播放音频
                if (!mediaPlayer.isPlaying()){
                    mediaPlayer.start();
                }
                break;
            case R.id.pause:
                //判断当前mediaPlayer是否正在播放音频
                if (mediaPlayer.isPlaying()){
                    mediaPlayer.pause();
                }
                break;
            case R.id.stop:
                if (mediaPlayer.isPlaying()){
                    //将MediaPlayer对象重置到刚刚创建的状态
                    mediaPlayer.reset();
                    // 注意!reset之后也要重新prepare才行
                    initMediaPlayer();
                }
                break;
                default:
                    break;
        }
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mediaPlayer!=null){
            ////注意我们在后面的分屏中学到mediaPlayer不能在onPause()方法中结束
            //而应该在onDestory方法中结束!!!切记
            mediaPlayer.stop();
            //释放掉与MediaPlayer对象相关的资源
            mediaPlayer.release();
        }
    }
}

记得在AndroidManifest.xml文件中声明权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

播放视频

播放视频三部曲
1.首先创建一个VideoView对象
2.调用setDataSource()方法来设置视频文件的路径
3.调用start()方法就可以开始播放视频,调用pause()方法就会暂停播放,调用resume()方法就会重头开始播放
public class VideoViewActivity extends AppCompatActivity implements View.OnClickListener {
    private Button mBtnPause;
    private Button mBtnPlay;
    private Button mBtnReplay;
    private VideoView videoView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_view);
        mBtnPlay = findViewById(R.id.playvideo);
        mBtnPause = findViewById(R.id.pausevideo);
        mBtnReplay = findViewById(R.id.replayvideo);
        videoView = findViewById(R.id.video_view);
        mBtnPlay.setOnClickListener(this);
        mBtnPause.setOnClickListener(this);
        mBtnReplay.setOnClickListener(this);
        if (ContextCompat.checkSelfPermission(VideoViewActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(VideoViewActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        } else {
            //初始化VideoView
            initVideoPath();
        }
    }
    private void initVideoPath() {
        //我们创建了一个File对象,用于存放视频文件,这个视频文件的名字叫video.mp4
        //通过创建一个File对象来指定视频文件的路径
       //实现在SD卡的根目录下放置一个名为video.mp4的视频文件
        File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),"video.mp4");
        //指定视频文件的路径---其实就是手机内部存储目录文件下
        videoView.setVideoPath(file.getPath());
        Log.i("视频文件路径", file.getPath());
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    initVideoPath();
                } else {
                    Toast.makeText(this, "拒绝权限无法使用程序", Toast.LENGTH_SHORT).show();
                    //结束程序
                    finish();
                }
                break;
                default:
        }
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.playvideo:
                //原来是这个地方出了问题  草草草草草以前是videoView.isPlaying()因为我们刚开始没有点play所以应该是不播放的!!
                if (!videoView.isPlaying())
                    videoView.start();
                Toast.makeText(this,"开始播放视频",Toast.LENGTH_SHORT).show();
                break;
            case R.id.pausevideo:
                if (videoView.isPlaying())
                    videoView.pause();
                break;
            case R.id.replayvideo:
                if (videoView.isPlaying())
                    videoView.resume();
                break;
        }
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (videoView != null) {
            //注意我们在后面的分屏中学到VideoView不能在onPause()方法中结束
            //而应该在onDestory方法中结束!!!切记
            videoView.suspend();
        }
    }
}

记得在AndroidManifest.xml文件中声明权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

通知部分

通知时Android系统中比较有特色的功能,当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现.发出一条通知后,手机最上方的状态栏会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容.

通知的使用场合

我们可以在活动中创建通知,也可以在广播接收器里创建,还可以在服务中创建.相对于广播接收器和服务,在活动中创建通知还是比较少的,因为一般只有程序进入后台的时候我们才需要使用通知.

创建通知的步骤

  • 1获取NotificationManager ---Context类的getSystemService(Context.NOTIFICATION_SERVICE)方法
  • 2使用一个Builder构造器来创建Notification对象
  • 3在Notification对象的build()方法之前连缀很多的设置方法来创建一个丰富的Notification对象
  • 4调用NotificationManager的notify()方法让通知显示出来

Notification的相关代码如下(包含高阶技巧的一系列设置方法)

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button mBtnSendnotice;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtnSendnotice = findViewById(R.id.btn_send_notice);
        mBtnSendnotice.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_send_notice:
                //Intent的意图是从MainActivity显式跳转到NotificationActivity
                Intent intent = new Intent(MainActivity.this, NotificationActivity.class);
                //创建PendingIntent,通过设置PendingIntent来实现通知的点击效果
                //PendingIntent可以认为是一个延时的Intent
                //PendingIntent的第三个参数是一个Intent对象,可以通过这个对象构建出PendingIntent的意图---从MainActivity显式跳转到NotificationActivity
                //PendingIntent它提供了几个静态方法来获取PendingIntent的实例,可以根据需求使用getActivity(),getBroadcast(),还是getService()方法
                //我们这个地方因为是在Activity中所以使用了getActivity()方法
                PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, intent, 0);
                //1.获取通知管理器
                NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                //2.实例化通知栏构造器
                //使用一个Builder构造器来创建Notification对象
                Notification notification = new NotificationCompat.Builder(MainActivity.this)
                        //指定通知的标题内容
                        .setContentTitle("This is content title")
                        //指定通知的正文内容
                        .setContentText(" 学习如何去新建一个通知,发送并同步数据,并且运动声音震动,获取安卓官方IDE,和发展着工具去新建app给安卓")
                        //指定通知被创建时间,以毫秒为单位
                        .setWhen(System.currentTimeMillis())
                        //设置通知小图标
                        .setSmallIcon(R.mipmap.ic_launcher)
                        //设置通知大图标
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                        //通过PendingIntent构建出一个延迟执行的"意图",当用户点击这条通知的时候,就会执行相应的逻辑
                        .setContentIntent(pendingIntent)
                        //让系统状态栏上的通知图标点击后消失
                        .setAutoCancel(true)
                        //在通知发出的时候播放一段音频,该方法接收一个Uri参数,所以在指定音频文件的时候还需要先获取到音频文件所对应的URI
                        .setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg")))
                        //设置在通知到来的时候让手机震动,让手机震动需要在AndroidManifest.xml文件中设置相应的权限
                        .setVibrate(new long[]{0, 1000, 1000, 1000})
                        //在通知到来的时候控制手机LED灯的显示
                        .setLights(Color.GREEN, 1000, 1000)
                        //该方法允许我们构建出富文本的通知内容,即通知中不仅可以有文字图标,还可以有更多的东西
                        .setStyle(new NotificationCompat.BigTextStyle().bigText("学习如何去新建一个通知,发送并同步数据,并且运动声音震动,获取安卓官方IDE,和发展着工具去新建app给安卓"))
                        .setStyle(new NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_background)))
                        //设置通知的重要程度
                        .setPriority(NotificationCompat.PRIORITY_MAX)
                        .build();
                //3.调用NotificationManager的notify()方法让通知显示出来
                //第一个参数id保证每条通知所指定的id都是不同的,第二个参数是Notification对象
                manager.notify(1, notification);
                break;
            default:
                break;
        }
    }
}

PendingIntent的用法-----PendingIntent可以认为是一个延迟执行的Intent,它主要提供了几个静态方法用于获取PendingIntent的实例,根据需要使用getActivity(),getBroadcast(),还是getService()方法,如果你想在activity中发送那种有点击响应的通知的话那么你就用getActivity()方法来获取PendingIntent的实例然后创建通知;Broadcast和Service同理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,835评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,900评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,481评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,303评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,375评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,729评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,877评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,633评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,088评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,443评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,563评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,251评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,827评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,712评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,943评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,240评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,435评论 2 348

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,626评论 18 139
  • 第六章内容主讲数据持久化技术 一、数据持久化技术简介   数据持久化技术就是将瞬时数据(存储在内存中,有可能会因为...
    radish520like阅读 970评论 0 0
  • 本篇文章主要介绍以下几个知识点:SQLite 数据库存储;开源数据库框架 LitePal。 6.3 SQLite ...
    开心wonderful阅读 1,091评论 7 5
  • 夜 又从远方走到了我的桌前 我们静静的坐着 谁也不说一句话 男人的嗓门和孩子的哭声 敲打在夜的锣面上 像是自然本...
    桔树上阅读 176评论 0 2
  • 孤雁 崔涂几行归塞尽,念尔独何之。暮雨相呼失,寒塘欲下迟。渚云低暗度,关月冷相随。未必逢矰缴,孤飞自可疑。 【译文...
    空中语阅读 722评论 0 0