问题
I want to add "prev/next" functionality to my master/detail view. The master loads a GridView (with thumbnails), which -- when clicked -- start a DetailActivity via an URI (essentially containing the corresponding image ID).
What I want is for the detail activity/fragment to be able to jump to previous/next image, directly -- without having to go back and forth through the master view. Ideally, I want to capture swipe gestures.
I've read that the best way to do this is to use a ViewPager, with FragmentStatePagerAdapter to hold the child views. But, I'd like to understand why my first attempt isn't working (below). (Also, I hope to avoid major reworking of the code, unless I have to.) While my attempt does work, I get an OutOfMemory error/crash after only a few swipes:
10-13 13:11:21.441 4149-4149/com.example.android.galleri.app E/art﹕ Throwing OutOfMemoryError "Failed to allocate a 33177612 byte allocation with 12795712 free bytes and 12MB until OOM"
10-13 13:11:21.455 4149-4149/com.example.android.galleri.app I/art﹕ Clamp target GC heap from 195MB to 192MB
10-13 13:11:21.456 4149-4149/com.example.android.galleri.app I/art﹕ Alloc partial concurrent mark sweep GC freed 6(192B) AllocSpace objects, 0(0B) LOS objects, 6% free, 179MB/192MB, paused 903us total 10.248ms
10-13 13:11:21.474 4149-4149/com.example.android.galleri.app I/art﹕ Clamp target GC heap from 195MB to 192MB
10-13 13:11:21.474 4149-4149/com.example.android.galleri.app I/art﹕ Alloc concurrent mark sweep GC freed 3(96B) AllocSpace objects, 0(0B) LOS objects, 6% free, 179MB/192MB, paused 975us total 18.144ms
10-13 13:11:21.474 4149-4149/com.example.android.galleri.app I/art﹕ Forcing collection of SoftReferences for 31MB allocation
10-13 13:11:21.490 4149-4149/com.example.android.galleri.app I/art﹕ Clamp target GC heap from 195MB to 192MB
10-13 13:11:21.490 4149-4149/com.example.android.galleri.app I/art﹕ Alloc concurrent mark sweep GC freed 3(96B) AllocSpace objects, 0(0B) LOS objects, 6% free, 179MB/192MB, paused 1.060ms total 15.295ms
10-13 13:11:21.490 4149-4149/com.example.android.galleri.app E/art﹕ Throwing OutOfMemoryError "Failed to allocate a 33177612 byte allocation with 12795712 free bytes and 12MB until OOM"
10-13 13:11:21.490 4149-4149/com.example.android.galleri.app D/skia﹕ --- decoder->decode returned false
10-13 13:11:21.491 4149-4149/com.example.android.galleri.app D/AndroidRuntime﹕ Shutting down VM
--------- beginning of crash
10-13 13:11:21.492 4149-4149/com.example.android.galleri.app E/AndroidRuntime﹕ FATAL EXCEPTION: main
Process: com.example.android.galleri.app, PID: 4149
java.lang.OutOfMemoryError: Failed to allocate a 33177612 byte allocation with 12795712 free bytes and 12MB until OOM
at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
at android.graphics.BitmapFactory.decodeStreamInternal(BitmapFactory.java:635)
at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:611)
at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:391)
at android.graphics.BitmapFactory.decodeFile(BitmapFactory.java:417)
at com.example.android.galleri.app.DetailFragment.onLoadFinished(DetailFragment.java:302)
at com.example.android.galleri.app.DetailFragment.onLoadFinished(DetailFragment.java:43)
at android.support.v4.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:427)
at android.support.v4.app.LoaderManagerImpl$LoaderInfo.onLoadComplete(LoaderManager.java:395)
at android.support.v4.content.Loader.deliverResult(Loader.java:104)
at android.support.v4.content.CursorLoader.deliverResult(CursorLoader.java:73)
at android.support.v4.content.CursorLoader.deliverResult(CursorLoader.java:35)
at android.support.v4.content.AsyncTaskLoader.dispatchOnLoadComplete(AsyncTaskLoader.java:223)
at android.support.v4.content.AsyncTaskLoader$LoadTask.onPostExecute(AsyncTaskLoader.java:61)
at android.support.v4.content.ModernAsyncTask.finish(ModernAsyncTask.java:461)
at android.support.v4.content.ModernAsyncTask.access$500(ModernAsyncTask.java:47)
at android.support.v4.content.ModernAsyncTask$InternalHandler.handleMessage(ModernAsyncTask.java:474)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:211)
at android.app.ActivityThread.main(ActivityThread.java:5389)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1020)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:815)
Here is the relevant (I hope) code.
Note the
public void onItemSelected(Uri contentUri)
call from MainActivity, which does the normal loading of an image "into" the DetailFragment.This method is called (via callback/interface) from the PhotoFragment: The GridView's
onClick
handler, created in theonCreate
method. That's the normal master-to-detail loading of images, works fine.Then the other way: DetailActivity captures the swipe gestures, and calls the
changeImage
method in the DetailFragment (via member object/reference).The DetailFragment's
changeImage
method fetches the cursor used in the PhotoFragment, via the Utility class (container object). Then, it gets the next/previous row and constructs a new URI with that ID, and starts (restarts?) the DetailActivity with that URI.
These restarts seem to accumulate memory, until the app crashes. The crash (OOM error) always occurs at the same line of DetailFragment, in the part where I load the Bitmap into the PhotoView:
// OOM crash always occurs HERE (after ~5 swipes)
thumbBitmap = BitmapFactory.decodeFile(thumbData);
MainActivity.java
public class MainActivity extends ActionBarActivity implements PhotoFragment.Callback {
private static final String DETAILFRAGMENT_TAG = "DFTAG";
private boolean mTwoPane;
private PhotoFragment mFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (findViewById(R.id.photo_detail_container) != null) {
mTwoPane = true;
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.photo_detail_container, new DetailFragment(), DETAILFRAGMENT_TAG)
.commit();
}
} else {
mTwoPane = false;
}
mFragment = ((PhotoFragment) getSupportFragmentManager()
.findFragmentById(R.id.fragment_photo));
mFragment.setTwoPaneUI(mTwoPane);
}
@Override
// this method is called from PhotoFragment (GridView) via a callback interface
public void onItemSelected(Uri contentUri) {
if (mTwoPane) {
Bundle args = new Bundle();
args.putParcelable(DetailFragment.DETAIL_URI, contentUri);
DetailFragment fragment = new DetailFragment();
fragment.setArguments(args);
fragment.setTwoPane(true);
getSupportFragmentManager().beginTransaction()
.replace(R.id.photo_detail_container, fragment, DETAILFRAGMENT_TAG)
.commit();
} else {
// launch DetailActivity
Intent intent = new Intent(this, DetailActivity.class)
.setData(contentUri);
startActivity(intent);
}
}
}
PhotoFragment.java
public class PhotoFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private final static int LOADER_ID = 87;
private PhotoAdapter mPhotoAdapter;
private int mPosition;
private GridView mGridView;
private TextView mEmptyView;
private int count_mem = -1;
private static final String SELECTED_KEY = "POSITION";
private boolean mTwoPane;
private boolean DF_hidden = false;
// these are the data we want from MediaStore
private final static String[] THUMBNAIL_COLUMNS = {
//PhotoContract.ThumbEntry.COLUMN_THUMB_ID,
PhotoContract.ThumbEntry.COLUMN_THUMB_DATA,
PhotoContract.ThumbEntry.COLUMN_IMAGE_PK,
PhotoContract.ThumbEntry.COLUMN_TITLE,
PhotoContract.ThumbEntry.COLUMN_DESC,
PhotoContract.ThumbEntry.COLUMN_DATE,
PhotoContract.ThumbEntry.COLUMN_FILENAME,
PhotoContract.ThumbEntry.COLUMN_DATA,
PhotoContract.ThumbEntry.COLUMN_IMAGESEQUENCE
};
static final int COL_THUMB_DATA = 0;
static final int COL_IMAGE_ID = 1;
static final int COL_TITLE = 2;
static final int COL_DESC = 3;
static final int COL_DATE = 4;
static final int COL_FILENAME = 5;
static final int COL_DATA = 6;
static final int COL_IMAGESEQUENCE = 7;
public void setTwoPaneUI(boolean pTwoPane) {
mTwoPane = pTwoPane;
}
// interfaces
public interface Callback {
public void onItemSelected(Uri photoUri);
}
public interface FragmentCallback {
public void onTaskDone(int count);
}
public PhotoFragment() {
}
public int getPosition() {
return mPosition;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
getLoaderManager().initLoader(LOADER_ID, null, this);
super.onActivityCreated(savedInstanceState);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onDestroy() {
super.onDestroy();
}
public void restartLoader() {
getLoaderManager().restartLoader(LOADER_ID, null, this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
if (mPosition != GridView.INVALID_POSITION) {
outState.putInt(SELECTED_KEY, mPosition);
}
super.onSaveInstanceState(outState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
mPhotoAdapter = new PhotoAdapter(getActivity(), null, 0);
GridView gridView = (GridView) rootView.findViewById(R.id.listview_photo);
gridView.setAdapter(mPhotoAdapter);
mEmptyView = (TextView) rootView.findViewById(R.id.list_empty);
mGridView = gridView;
// handle user clicking on an image
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView parent, View view, int position, long id) {
Cursor cursor = (Cursor) parent.getItemAtPosition(position);
if (cursor != null) {
Uri baseUri = PhotoContract.PhotoEntry.buildPhotoUriWithId(cursor.getLong(COL_IMAGE_ID));
Log.v("gallery", "PhotoFragment.onItemClick() image_id = " + cursor.getLong(COL_IMAGE_ID)
+ ", baseUri = " + baseUri.toString());
// make sure detail fragment is visible
if (mTwoPane) showDetailFragment();
else Utility.setPosition(position); // else (single-pane): store position clicked!
((Callback)getActivity()).onItemSelected(baseUri);
}
mPosition = position;
}
});
if (savedInstanceState != null && savedInstanceState.containsKey(SELECTED_KEY)) {
mPosition = savedInstanceState.getInt(SELECTED_KEY);
}
return rootView;
}
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri thumbs_uri = PhotoContract.ThumbEntry.CONTENT_URI;
return new CursorLoader(getActivity(), thumbs_uri, THUMBNAIL_COLUMNS,
null,null, // read everything (all thumbnails)
null
);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (cursor != null) {
// register observer onto cursor
cursor.setNotificationUri(getActivity().getContentResolver(), PhotoContract.ThumbEntry.CONTENT_URI);
mPhotoAdapter.swapCursor(cursor);
if (mPosition != GridView.INVALID_POSITION) {
mGridView.setSelection(mPosition); // scroll into view
}
}
// save (set) cursor in Utility
Utility.setCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
mPhotoAdapter.swapCursor(null);
}
}
DetailActivity.java
public class DetailActivity extends ActionBarActivity {
private static final String DETAILFRAGMENT_TAG = "DFTAG";
private float x1,x2, y1,y2;
static final int MIN_DISTANCE = 150;
private DetailFragment mFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
if (savedInstanceState == null) {
Bundle arguments = new Bundle();
arguments.putParcelable(DetailFragment.DETAIL_URI, getIntent().getData());
//DetailFragment fragment = new DetailFragment();
mFragment = new DetailFragment();
mFragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.add(R.id.photo_detail_container, mFragment, DETAILFRAGMENT_TAG)
.commit();
}
}
// http://stackoverflow.com/questions/6645537/how-to-detect-the-swipe-left-or-right-in-android
@Override
public boolean onTouchEvent(MotionEvent event)
{
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
x1 = event.getX();
y1 = event.getY();
break;
case MotionEvent.ACTION_UP:
x2 = event.getX();
y2 = event.getY();
float deltaX = x2 - x1;
if (Math.abs(deltaX) > MIN_DISTANCE) {
if (x2 > x1)
mFragment.changeImage(-1); // move backwards
else
mFragment.changeImage(1); // move forwards
}
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_detail, menu);
return true;
}
}
DetailFragment.java
public class DetailFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final String PHOTOFRAGMENT_TAG = "PFTAG";
static final String DETAIL_URI = "URI";
private static final int DETAIL_LOADER = 0;
private ImageView mPhotoView, mCloseIcon;
private TextView mImageSizeView, mImageDateView;
public String mMemTitle, mMemDesc;
// data for sharing
private Long mImageID;
private ShareActionProvider mShareActionProvider;
private static final String[] DETAIL_COLUMNS = {
PhotoContract.PhotoEntry.COLUMN_IMAGE_ID,
PhotoContract.PhotoEntry.COLUMN_DISPLAY_NAME,
PhotoContract.PhotoEntry.COLUMN_DATA,
PhotoContract.PhotoEntry.COLUMN_DESC,
PhotoContract.PhotoEntry.COLUMN_DATE_TAKEN,
PhotoContract.PhotoEntry.COLUMN_DATE_ADDED,
PhotoContract.PhotoEntry.COLUMN_TITLE,
PhotoContract.PhotoEntry.COLUMN_SIZE,
PhotoContract.PhotoEntry.COLUMN_ORIENTATION,
PhotoContract.PhotoEntry.COLUMN_HEIGHT,
PhotoContract.PhotoEntry.COLUMN_WIDTH
};
static final int COL_IMAGE_ID = 0;
static final int COL_DISPLAY_NAME = 1;
static final int COL_DATA = 2;
static final int COL_DESC = 3;
static final int COL_DATE_TAKEN = 4;
static final int COL_DATE_ADDED = 5;
static final int COL_TITLE = 6;
static final int COL_SIZE = 7;
static final int COL_ORIENTATION = 8;
static final int COL_HEIGHT = 9;
static final int COL_WIDTH = 10;
static final int COL_THUMB_ID = 0;
static final int COL_THUMB_DATA = 1;
static final int COL_THUMB_IMAGE_ID = 2;
private Uri mUri;
private boolean noMedia = false;
private boolean mTwoPane = false;
public DetailFragment() {
setHasOptionsMenu(true); // only share button, though
}
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.detailfragment, menu);
MenuItem menuItem = menu.findItem(R.id.action_share);
mShareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(menuItem);
if (mFilePath != null) {
mShareActionProvider.setShareIntent(createShareForecastIntent());
}
}
public ImageView getPhotoView() {
return mPhotoView;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Bundle arguments = getArguments();
if (arguments != null) {
mUri = arguments.getParcelable(DetailFragment.DETAIL_URI);
}
// get ID from URI
if (mUri != null) {
mImageID = PhotoContract.PhotoEntry.getImageIdFromUri(mUri);
}
View rootView = inflater.inflate(R.layout.fragment_detail, container, false);
mPhotoView = (ImageView) rootView.findViewById(R.id.pic_frame);
mImageSizeView = (TextView) rootView.findViewById(R.id.imgSize_textview);
mImageDateView = (TextView) rootView.findViewById(R.id.date_textview);
return rootView;
}
// user swipes; go to prev (direction = -1) or next (direction = 1) image:
public void changeImage(int direction) {
Cursor cursor = Utility.getCursor();
Long oldId = mImageID;
if (cursor == null)
return;
boolean result;
switch (direction) {
case -1:
if (cursor.isFirst()) return;
result = cursor.moveToPrevious();
break;
case 1:
if (cursor.isLast()) return;
result = cursor.moveToNext();
break;
default:
return;
}
Log.v("gallery", "DetailFragment.changeImage(" + direction + "), old id "
+ oldId + " => IMAGE_ID = " + cursor.getLong(PhotoFragment.COL_IMAGE_ID));
if (result) {
mUri = PhotoContract.PhotoEntry.buildPhotoUriWithId(cursor.getLong(PhotoFragment.COL_IMAGE_ID));
Utility.setPosition(cursor.getPosition()); // store position clicked!
// re-launch DetailActivity
Intent intent = new Intent(getActivity(), DetailActivity.class)
.setData(mUri);
startActivity(intent);
}
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (mUri != null) {
return new CursorLoader(getActivity(),
mUri, // URI passed in
DETAIL_COLUMNS, null, null, null);
} else {
mUri = PhotoContract.PhotoEntry.CONTENT_URI; // means, default (first) image
return new CursorLoader(getActivity(),
mUri, // "default URI": show first image...
DETAIL_COLUMNS, null, null, null);
}
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor data) {
if (data == null || !data.moveToFirst()) {
return;
}
String thumbData = data.getString(COL_DATA);
if (thumbData != null) {
Bitmap thumbBitmap;
try {
thumbBitmap = BitmapFactory.decodeFile(thumbData); // <--- OOM crash always occurs HERE (after ~5 swipes)
mPhotoView.setImageBitmap(thumbBitmap);
} catch (Exception e) {
return;
}
}
String metaDataText = data.getString(COL_WIDTH) + " x " + data.getString(COL_HEIGHT) + ", "
+ Math.round(data.getLong(COL_SIZE) / 1024.0) + "kb";
mImageSizeView.setText(metaDataText, TextView.BufferType.SPANNABLE);
mImageDateView.setText(DateFormat.format("d/M-yyyy",
new Date(data.getLong(COL_DATE_TAKEN))).toString());
if (mShareActionProvider != null)
mShareActionProvider.setShareIntent(createShareForecastIntent());
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
getLoaderManager().initLoader(DETAIL_LOADER, null, this);
super.onActivityCreated(savedInstanceState);
}
public void setTwoPane(boolean b) {
mTwoPane = b;
}
}
来源:https://stackoverflow.com/questions/33106813/adding-prev-next-buttons-to-detail-fragment-of-master-detail-activity