关于RecyclerViewCursorAdapter
RecyclerView是谷歌推荐使用的也是现在比较常用的控件,用来代替ListView。CursorAdapter经常会配合数据库使用,然而在RecyclerView中使用CursorAdapter时会出错!查了一下,CursorAdapter只和ListView适配,RecyclerView并不兼容,看来需要一个改造过的CursorAdapter。还好,Github上有大神实现了RecyclerViewCursorAdapter:传送门。在里面找到主要用到了两个类,代码这里就直接贴出来了:
RecyclerViewCursorAdapter.java
public abstract class RecyclerViewCursorAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> implements Filterable,
CursorFilter.CursorFilterClient {
/**
* Call when bind view with the cursor
*
* @param holder
* @param cursor
*/
public abstract void onBindViewHolder(VH holder, Cursor cursor);
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected boolean mDataValid;
/**
* The current cursor
*/
protected Cursor mCursor;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected Context mContext;
/**
* The row id column
*/
protected int mRowIDColumn;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected ChangeObserver mChangeObserver;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected DataSetObserver mDataSetObserver;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected CursorFilter mCursorFilter;
/**
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected FilterQueryProvider mFilterQueryProvider;
/**
* If set the adapter will register a content observer on the cursor and will call
* {@link #onContentChanged()} when a notification comes in. Be careful when
* using this flag: you will need to unset the current Cursor from the adapter
* to avoid leaks due to its registered observers. This flag is not needed
* when using a CursorAdapter with a
* {@link android.content.CursorLoader}.
*/
public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
/**
* Recommended constructor.
*
* @param c The cursor from which to get the data.
* @param context The context
* @param flags Flags used to determine the behavior of the adapter;
* Currently it accept {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
*/
public RecyclerViewCursorAdapter(Context context, Cursor c, int flags) {
init(context, c, flags);
}
void init(Context context, Cursor c, int flags) {
boolean cursorPresent = c != null;
mCursor = c;
mDataValid = cursorPresent;
mContext = context;
mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
mChangeObserver = new ChangeObserver();
mDataSetObserver = new MyDataSetObserver();
} else {
mChangeObserver = null;
mDataSetObserver = null;
}
if (cursorPresent) {
if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
}
setHasStableIds(true);
}
/**
* Returns the cursor.
*
* @return the cursor.
*/
@Override
public Cursor getCursor() {
return mCursor;
}
/**
* @see RecyclerView.Adapter#getItemCount()
*/
@Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
} else {
return 0;
}
}
/**
* @param position Adapter position to query
* @return
* @see RecyclerView.Adapter#getItemId(int)
*/
@Override
public long getItemId(int position) {
if (mDataValid && mCursor != null) {
if (mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIDColumn);
} else {
return 0;
}
} else {
return 0;
}
}
@Override
public void onBindViewHolder(VH holder, int position) {
if (!mDataValid) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
onBindViewHolder(holder, mCursor);
}
/**
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be
* closed.
*
* @param cursor The new cursor to be used
*/
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
/**
* Swap in a new Cursor, returning the old Cursor. Unlike
* {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
* closed.
*
* @param newCursor The new cursor to be used.
* @return Returns the previously set Cursor, or null if there wasa not one.
* If the given new Cursor is the same instance is the previously set
* Cursor, null is also returned.
*/
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
Cursor oldCursor = mCursor;
if (oldCursor != null) {
if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (newCursor != null) {
if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
// notify the observers about the new cursor
notifyDataSetChanged();
} else {
mRowIDColumn = -1;
mDataValid = false;
// notify the observers about the lack of a data set
notifyDataSetChanged();
// notifyDataSetInvalidated();
}
return oldCursor;
}
/**
* <p>Converts the cursor into a CharSequence. Subclasses should override this
* method to convert their results. The default implementation returns an
* empty String for null values or the default String representation of
* the value.</p>
*
* @param cursor the cursor to convert to a CharSequence
* @return a CharSequence representing the value
*/
public CharSequence convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
/**
* Runs a query with the specified constraint. This query is requested
* by the filter attached to this adapter.
* <p/>
* The query is provided by a
* {@link FilterQueryProvider}.
* If no provider is specified, the current cursor is not filtered and returned.
* <p/>
* After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
* and the previous cursor is closed.
* <p/>
* This method is always executed on a background thread, not on the
* application's main thread (or UI thread.)
* <p/>
* Contract: when constraint is null or empty, the original results,
* prior to any filtering, must be returned.
*
* @param constraint the constraint with which the query must be filtered
* @return a Cursor representing the results of the new query
* @see #getFilter()
* @see #getFilterQueryProvider()
* @see #setFilterQueryProvider(FilterQueryProvider)
*/
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
if (mFilterQueryProvider != null) {
return mFilterQueryProvider.runQuery(constraint);
}
return mCursor;
}
public Filter getFilter() {
if (mCursorFilter == null) {
mCursorFilter = new CursorFilter(this);
}
return mCursorFilter;
}
/**
* Returns the query filter provider used for filtering. When the
* provider is null, no filtering occurs.
*
* @return the current filter query provider or null if it does not exist
* @see #setFilterQueryProvider(FilterQueryProvider)
* @see #runQueryOnBackgroundThread(CharSequence)
*/
public FilterQueryProvider getFilterQueryProvider() {
return mFilterQueryProvider;
}
/**
* Sets the query filter provider used to filter the current Cursor.
* The provider's
* {@link FilterQueryProvider#runQuery(CharSequence)}
* method is invoked when filtering is requested by a client of
* this adapter.
*
* @param filterQueryProvider the filter query provider or null to remove it
* @see #getFilterQueryProvider()
* @see #runQueryOnBackgroundThread(CharSequence)
*/
public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
mFilterQueryProvider = filterQueryProvider;
}
/**
* Called when the {@link ContentObserver} on the cursor receives a change notification.
* The default implementation provides the auto-requery logic, but may be overridden by
* sub classes.
*
* @see ContentObserver#onChange(boolean)
*/
protected abstract void onContentChanged();
private class ChangeObserver extends ContentObserver {
public ChangeObserver() {
super(new Handler());
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
onContentChanged();
}
}
private class MyDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
mDataValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
mDataValid = false;
notifyDataSetChanged();
// notifyDataSetInvalidated();
}
}
}
CursorFilter.java
class CursorFilter extends Filter {
CursorFilterClient mClient;
interface CursorFilterClient {
CharSequence convertToString(Cursor cursor);
Cursor runQueryOnBackgroundThread(CharSequence constraint);
Cursor getCursor();
void changeCursor(Cursor cursor);
}
CursorFilter(CursorFilterClient client) {
mClient = client;
}
@Override
public CharSequence convertResultToString(Object resultValue) {
return mClient.convertToString((Cursor) resultValue);
}
@Override
protected FilterResults performFiltering(CharSequence constraint) {
Cursor cursor = mClient.runQueryOnBackgroundThread(constraint);
FilterResults results = new FilterResults();
if (cursor != null) {
results.count = cursor.getCount();
results.values = cursor;
} else {
results.count = 0;
results.values = null;
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
Cursor oldCursor = mClient.getCursor();
if (results.values != null && results.values != oldCursor) {
mClient.changeCursor((Cursor) results.values);
}
}
}
把上面这两个类直接放到工程中,让RecyclerView的Adapter继承RecyclerViewCursorAdapter,然后就可以像ListView的CursorAdapter那样使用它了。
Loader机制
Loader,顾名思义,就是一个加载器。是Android 3.0后推出的一个用于异步加载数据的机制。可用与Activity和Fragment中。它能检测数据源,当数据源内容改变时它们能够传递新的结果,当配置改变后需要重新创建时,它们会重新连接到最后一个Loader的游标。这样,它们不需要重新查询它们的数据。常用于ContentProvider、CursorAdapter和数据库配合使用。Google提供了一个标准的Loader,另外还有一个抽象类AsyncTaskLoader。本文只是简单的谈谈这个Loader的使用,即CursorLoader,它是Loader的标准实现,如果你的数据能够用Cursor表示,比如来自SQLiteDatabase的数据就是标准的Cursor,那么这个类对你而言就够用了。
我们就来配合前面提到的RecyclerViewCursorAdapter,让Loader机制结合RecyclerViewCursorAdapter写一个demo,上图看看效果:
简单使用的过程
建一个bean类,Book.java:
package com.sonnyzoom.loaderdemo.bean;
/**
* Created by zoom on 2016/3/30.
*/
public class Book {
private String id;
private String name;
private int price;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
数据库:BookDBHelper.java:
package com.sonnyzoom.loaderdemo.db;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
/**
* Created by zoom on 2016/3/30.
*/
public class BookDBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "Book.db";
private static final int DATABASE_VERSION = 1;
public static final String TABLE_NAME = "BookInfo";
public static final String ID = "_id";
public static final String NAME = "name";
public static final String PRICE = "price";
private static final String CREATE_DATABASE="CREATE TABLE "+TABLE_NAME+" ( "
+ID+" INTEGER PRIMARY KEY AUTOINCREMENT ,"
+NAME+" TEXT,"
+PRICE+" INTEGER)";
private volatile static BookDBHelper helper;
private BookDBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public static BookDBHelper getInstance(Context context) {
if (helper == null) {
synchronized (BookDBHelper.class) {
if (helper == null) {
helper = new BookDBHelper(context);
}
}
}
return helper;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_DATABASE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
一个基本的ContentProvider类,关于ContentProvider就不多说了,不要忘记在AndroidManifest里面注册。
package com.sonnyzoom.loaderdemo.provider;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.sonnyzoom.loaderdemo.db.BookDBHelper;
/**
* Created by zoom on 2016/3/30.
*/
public class BookProvider extends ContentProvider {
private static final String AUTHORITY="com.sonnyzoom.loaderdemo.provider.bookprovider";
public static final Uri URI_BOOK_ALL=Uri.parse("content://"+AUTHORITY+"/book");
private static UriMatcher matcher;
private BookDBHelper helper;
private SQLiteDatabase db;
private static final int BOOK_ALL=0;
private static final int BOOK_ONE=1;
static {
matcher=new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI(AUTHORITY,"book/",BOOK_ALL);
matcher.addURI(AUTHORITY,"book/#",BOOK_ONE);
}
@Override
public boolean onCreate() {
helper=BookDBHelper.getInstance(getContext());
return true;
}
@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
switch (matcher.match(uri)){
case BOOK_ALL:
break;
case BOOK_ONE:
long id= ContentUris.parseId(uri);
selection="_id=?";
selectionArgs=new String[]{String.valueOf(id)};
break;
default:
throw new IllegalArgumentException("Wrong Uri:"+uri);
}
db=helper.getReadableDatabase();
Cursor cursor=db.query(BookDBHelper.TABLE_NAME,projection,selection,selectionArgs,null,null,sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(),URI_BOOK_ALL);
return cursor;
}
@Nullable
@Override
public String getType(Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(Uri uri, ContentValues values) {
if (matcher.match(uri)!=BOOK_ALL){
throw new IllegalArgumentException("Wrong Uri:"+uri);
}
db=helper.getReadableDatabase();
long rowId = db.insert(BookDBHelper.TABLE_NAME, null, values);
if (rowId>0){
notifyDataSetChanged();
return ContentUris.withAppendedId(uri,rowId);
}
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
private void notifyDataSetChanged() {
getContext().getContentResolver().notifyChange(URI_BOOK_ALL,null);
}
}
然后是RecyclerView的适配器,BookAdapter.java,继承上面我们开头提过的RecyclerViewCursorAdapter:
package com.sonnyzoom.loaderdemo.adapter;
import android.content.Context;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.sonnyzoom.loaderdemo.R;
import com.sonnyzoom.loaderdemo.bean.Book;
import com.sonnyzoom.loaderdemo.db.BookDBHelper;
/**
* Created by zoom on 2016/3/30.
*/
public class BookAdapter extends RecyclerViewCursorAdapter<BookAdapter.BookViewHolder> {
private LayoutInflater inflater;
public BookAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
inflater=LayoutInflater.from(context);
}
@Override
public void onBindViewHolder(BookViewHolder holder, Cursor cursor) {
Book book=new Book();
book.setId(cursor.getString(cursor.getColumnIndex(BookDBHelper.ID)));
book.setName(cursor.getString(cursor.getColumnIndex(BookDBHelper.NAME)));
book.setPrice(cursor.getInt(cursor.getColumnIndex(BookDBHelper.PRICE)));
holder.name.setText(book.getName());
holder.price.setText(Integer.toString(book.getPrice()));
}
@Override
protected void onContentChanged() {}
@Override
public BookViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v=inflater.inflate(R.layout.book_item,parent,false);
return new BookViewHolder(v);
}
class BookViewHolder extends RecyclerView.ViewHolder{
public TextView name;
public TextView price;
public BookViewHolder(View itemView) {
super(itemView);
name= (TextView) itemView.findViewById(R.id.book_name);
price= (TextView) itemView.findViewById(R.id.book_price);
}
}
}
MainActivity里面主要的是CursorLoader :
private void initLoader() {
getLoaderManager().initLoader(1, null, new LoaderManager.LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader loader = new CursorLoader(MainActivity.this, BookProvider.URI_BOOK_ALL, null, null, null, null);
return loader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
adapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
});
}
MainActivity.java全部代码:
package com.sonnyzoom.loaderdemo;
import android.app.LoaderManager;
import android.content.ContentValues;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import com.sonnyzoom.loaderdemo.adapter.BookAdapter;
import com.sonnyzoom.loaderdemo.db.BookDBHelper;
import com.sonnyzoom.loaderdemo.provider.BookProvider;
public class MainActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private FloatingActionButton fab;
private BookAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
initViews();
initLoader();
if (fab != null) {
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showDialog();
}
});
}
}
private void showDialog() {
final AlertDialog dialog = new AlertDialog.Builder(this).create();
View v = getLayoutInflater().inflate(R.layout.dialog, null);
dialog.setView(v);
dialog.setCancelable(false);
dialog.show();
final EditText name = (EditText) v.findViewById(R.id.edit_name);
final EditText price = (EditText) v.findViewById(R.id.edit_price);
Button cancel = (Button) v.findViewById(R.id.btn_cancel);
Button save = (Button) v.findViewById(R.id.btn_save);
save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String mName = name.getText().toString();
String mPrice = price.getText().toString();
ContentValues values = new ContentValues();
values.put(BookDBHelper.NAME, mName);
values.put(BookDBHelper.PRICE, Integer.valueOf(mPrice));
getContentResolver().insert(BookProvider.URI_BOOK_ALL, values);
dialog.dismiss();
}
});
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
}
private void initLoader() {
getLoaderManager().initLoader(1, null, new LoaderManager.LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader loader = new CursorLoader(MainActivity.this, BookProvider.URI_BOOK_ALL, null, null, null, null);
return loader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
adapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
});
}
private void initViews() {
fab = (FloatingActionButton) findViewById(R.id.fab);
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
if (adapter == null) {
Cursor c = getContentResolver().query(BookProvider.URI_BOOK_ALL, null, null, null, null);
adapter = new BookAdapter(this, c, 1);
}
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setHasFixedSize(true);
recyclerView.setAdapter(adapter);
}
}
Loader机制的用法当然不只有这一种,还有更多的功能等你去发掘。这里只是浅谈,文章如有什么错误,欢迎在下方评论、交流!
文章源码地址:Github