Adding prev/next-buttons to “detail fragment” (of master/detail activity)

耗尽温柔 提交于 2019-12-11 21:16:47

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!