停止Exoplayer在ViewPager滑动时

17

我正在使用一个具有单个片段实例的ViewPager,在其中我展示像图像、视频、音频等Media文件。

我已经实现了ExoPlayer来处理VideoAudio文件。并使用Glide来处理图片。

为了避免内存泄漏,我在ItemViewerFragment.java中这样释放ExoPlayer对象:

 private void releasePlayer() {
        if (player != null) {
            player.release();
            player = null;
            trackSelector = null;
            simpleExoPlayerView.setPlayer(null);
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        if (player == null && currentGalleryModel != null) {
            initializePlayer();
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        if (player == null && currentGalleryModel != null) {
            initializePlayer();
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        releasePlayer();
    }

    @Override
    public void onStop() {
        super.onStop();
        releasePlayer();
    }

onViewCreated() 中,我像这样初始化视图:

 @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        if (currentGalleryModel.isVideo() || currentGalleryModel.isAudio()) {

            simpleExoPlayerView.setVisibility(View.VISIBLE);
            imageView.setVisibility(View.GONE);

            initializePlayer();
        } else if (currentGalleryModel.isImage() || currentGalleryModel.isGif()) {
            simpleExoPlayerView.setVisibility(View.GONE);
            imageView.setVisibility(View.VISIBLE);


            Glide.with(this)
                    .load(currentGalleryModel.getFilePath())
                    .placeholder(android.R.color.black)
                    .fitCenter()
                    .diskCacheStrategy(DiskCacheStrategy.NONE)
                    .into(imageView);

        }
    }

我正在使用FragmentStatePagerAdapeter。这是getItem方法:

@Override
public Fragment getItem(int position) {
    return ItemViewerFragment.newInstance(mItems.get(position));
}

我无法在第一次滑动 Viewpager 时检测到片段的 onPause。在第二次滑动时,视频/音频 文件停止播放。

在活动中,我尝试添加 .addOnPageChangeListener

@Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

     try {
        ((ItemViewerFragment)mAdapter.getItem(mPreviousPos)).imHiddenNow();
        } catch (Exception e) {
            e.printStackTrace();
        }
        mPreviousPos = position;
    }

ItemViewerFragment.java 中:

public void imHiddenNow(){
      releasePlayer();
    }

视频/音频仍在播放。

这里是 Screencast 的 Video Link

Demo 项目 GitHub 链接


你如何在onScrollListener中获取上一个位置? - Sarthak Gandhi
另外,我发现 getItem 方法每次都返回一个新实例,这可能是问题的原因。 - Sarthak Gandhi
在初始化时,我正在存储正在显示的“GalleryModel”的当前位置。因此,假设我的列表中有30个项目,我正在显示第5个。然后,在滚动时,先前的位置将是第5个,当前的位置将是第4个或第6个,具体取决于它向哪一侧滚动。 - Nilesh Deokar
获取项目看起来有些可疑,我会发布一个带有一些代码的答案,请尝试使用pageAdapter。 - Sarthak Gandhi
是的,我已经发布了所有的代码,如果您有任何问题,请回复。 - Sarthak Gandhi
显示剩余2条评论
5个回答

6

我将采用在 PagerAdapter 内部维护片段对象的 HashMap 的方法

  1. 声明一个接口:
interface FragmentLifecycle {
    void onPauseFragment()
}
  1. 在Fragment中实现接口。
public void onPauseFragment() {
    if (simpleExoPlayer != null){
        simpleExoPlayer.setPlayWhenReady(false);
    }
}
  1. Store all the fragment objects in a HashMap<Integer,Fragment> with there respective positions as a key. Declare hashmap inside PagerAdapter. Also declare one getter method for accessing fragment objects from hashmap. e.g.

    @Override
    public Fragment getItem(int position) {
        ItemViewerFragment fragment = ItemViewerFragment.newInstance(mItems.get(position));
        mFragments.put(position,fragment);
        return fragment;
    }
    

    public ItemViewerFragment getMapItem(int position) { return mFragments.get(position); }

  2. In activity where you have declared viewPager keep one variable currentPosition and implement ViewPager.OnPageChangeListener.

  3. Inside onPageSelected method,

        @Override
        public void onPageSelected(int position) {
            if(mAdapter.getMapItem(currentPosition) != null) (mAdapter.getMapItem(currentPosition)).onPauseFragment();
            currentPosition = position;
        }
    

这是一种反模式,getItem()在Android进程死亡后,FragmentPagerAdapter不会被调用以获取片段实例。该映射将不可避免地导致未来的NPE。 - EpicPandaForce

5

来自未来的编辑说明:在FragmentPagerAdapter中不要直接持有Fragment实例的引用,因为这可能会在进程死亡后导致崩溃。

以下是分页适配器的代码:

 class ViewPagerAdapter extends FragmentPagerAdapter {
    private final List<Fragment> mFragmentList = new ArrayList<>();
    private final List<String> mFragmentTitleList = new ArrayList<>();

    public ViewPagerAdapter(FragmentManager manager) {
        super(manager);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragmentList.get(position);
    }

    @Override
    public int getCount() {
        return mFragmentList.size();
    }

    public void addFragment(Fragment fragment, String title) {
        mFragmentList.add(fragment);
        mFragmentTitleList.add(title);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mFragmentTitleList.get(position);
    }
}

这是滚动监听器:

  viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
          //Stop media here.
        }

        @Override
        public void onPageSelected(int position) {
          //Save your previous position here.
        }

        @Override
        public void onPageScrollStateChanged(int state) {
         
        }
    });

对于媒体,您可以使用for循环一次性将所有片段添加到列表中,然后为了效率而使用此方法:

viewPager.setOffscreenPageLimit(3);

这将确保只有3个片段实例可用,这已经足够了。

如果要使用单个片段,我建议您按照以下方式操作:

 public MyFragment() {

}

//This is to send a file to the fragment if you need it.
public static MyFragment newInstance(File file) {
    MyFragment fragment = new MyFragment();
    Bundle bundle = new Bundle();
    bundle.putSerializable("file", file);
    fragment.setArguments(bundle);
    return fragment;
}

然后在Fragment的onCreate方法中,您可以像这样检索文件:

File file;
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    file = getArguments().getSerializable("file");
   
}

现在将你的片段添加到分页器中,方法如下:
 for (int i = 0; i < totalFiles; i++) {
            viewPagerAdapter.addFragment(MyFragment.newInstance(fileList.get(i));
        }

希望这有所帮助。

模型列表? - Nilesh Deokar
这就是为什么我发布了一个带有列表适配器的代码,你只需将所有片段对象添加到其中,然后滚动浏览即可。如果超出屏幕外限制,ViewPager会自动请求新实例。 - Sarthak Gandhi
有时候在向后滑动返回之后,视频视图无法重新创建。 - Nilesh Deokar
将您在onPageScrolled中编写的内容复制到onPageSelected中。 - Sarthak Gandhi
如果你有很多碎片,你肯定不想一次性将它们全部加载到内存中。你希望在用户阅读时加载和销毁碎片。在这种情况下,你将使用FragmentStatePagerAdapter。 - Faisal Naseer
显示剩余10条评论

2

我已经有一年多没有使用Exoplayer了,但我曾经解决过类似的问题。请注意API已经略有变化,所以请参考以下代码,了解如何实现潜在的解决方案。如果不起作用,请告诉我,我会进一步查看API并回复您。

解决方案如下:

private int mPlayerCurrentPosition;

private int getCurrentPlayerPosition() {
     return mExoPlayer.getCurrentPosition();
}

// call this from onPause
private void releaseExoplayer() {
     mPlayerCurrentPosition = getPlayerCurrentPosition();
     mExoPlayer.setPlayWhenReady(false);
     mExoPlayer.release(); // this will make the player object eligible for GC
}

private void resumePlaybackFromPreviousPosition(int prevPosition) {
    mExoPlayer.seekTo(mPlayerCurrentPosition );
}

谢谢!行为是一样的。我想在第一次滑动 viewPager 时暂停播放器,但它发生在第二次滑动中。 - Nilesh Deokar
你尝试过在viewPager的onPageScrolled和onPageScrollStateChanged回调中调用我答案中提到的releaseExoplayer()方法了吗? - Umer Farooq
是的,我做了。尽管onSwipe当前片段正在停止播放器。但是在同一片段上进行onSwipeBack并不会恢复播放器。我在片段的onResume中调用resumePlaybackFromPreviousPosition - Nilesh Deokar
这可能是因为您需要适当地重新初始化播放器。您没有分享重新初始化播放器所用的代码。另一件要记住的事情是,在onSaveInstance()回调中需要保留玩家的先前位置,并在片段的onActivityCreated()或onCreateView()中重新读取该值。如果您正确执行这些操作,理论上问题应该得到解决。 - Umer Farooq
我已经分享了详细的演示项目Github链接。如果你有时间,可以看一下。我会在实现你的建议后进行更新。谢谢! - Nilesh Deokar
1
为了从这个社区获得最好的支持,建议您编写简明的问题,并提供所有必要的代码片段。 - Umer Farooq

1
问题在于当ViewPager中的Fragment可见性更改时,onPauseonResume不会被调用。解决方案是添加2个可见性事件:losingVisibilitygainVisibility
为什么这是一个好的解决方案呢?因为你保持了框架管理Fragment缓存和生命周期的方式。我们只需要在我们的Fragment中添加所需的回调来暂停和恢复我们的媒体播放。
步骤如下:下面的代码只是我代码的解释。查看Step*.java类以查看完整实现。
  1. Create losingVisibility and gainVisibility methods in YourFragment.java:

    public class YourFragment extends Fragment {
    
        /**
          * This method is only used by viewpager because the viewpager doesn't call onPause after
          * changing the fragment
          */
        public void losingVisibility() {
            // IMPLEMENT YOUR PAUSE CODE HERE
            savePlayerState();
            releasePlayer();
        }
    
        /**
          * This method is only used by viewpager because the viewpager doesn't call onPause after
          * changing the fragment
        */
        public void gainVisibility() {
            // IMPLEMENT YOUR RESUME CODE HERE
            loadVideo();
        }
    }
    
  2. Call losingVisibility and gainVisibility every time a new page is selected (onPageSelected) in YourActivity.java:

    mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }
    
        @Override
        public void onPageSelected(int position) {
            YourFragment cachedFragmentLeaving = mYourPagerAdapter.getCachedItem(mCurrentItem);
            if (cachedFragmentLeaving != null) {
                cachedFragmentLeaving.losingVisibility();
            }
            mCurrentItem = position;
            YourFragment cachedFragmentEntering = mYourPagerAdapter.getCachedItem(mCurrentItem);
            if (cachedFragmentEntering != null) {
                cachedFragmentEntering.gainVisibility();
            }
        }
    
        @Override
        public void onPageScrollStateChanged(int state) {
        }
    });
    
  3. Add getCachedItem to YourPagerAdapter.java:

第三步是添加一个检索缓存片段的方法。为了实现这一点,我们必须缓存对创建的片段的引用(覆盖instantiateItem),并释放相同的引用(覆盖destroyItem)。
    public class YourPagerAdapter extends FragmentStatePagerAdapter {

            private SparseArray<YourFragment> mFragmentsHolded = new SparseArray<>();


        @NonNull
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            Object fragment = super.instantiateItem(container, position);
            if(fragment instanceof StepFragment) {
                mFragmentsHolded.append(position, (StepFragment) fragment);
            }
            return fragment;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            mFragmentsHolded.delete(position);
            super.destroyItem(container, position, object);
        }

        public YourFragment getCachedItem(int position) {
            return mFragmentsHolded.get(position, null);
        }
    }

0
我知道现在已经很晚了,但我希望这能对任何人有所帮助...
viewPager 会保持屏幕外的 fragment 启动状态...
所以你会有:
前一个屏幕外的 fragment(启动)--> 可见的 fragment(工作中)--> 下一个屏幕外的 fragment(启动)
解决方案是你可以重写 Fragment.setUserVisibleHint() 并处理播放/暂停视频。

即使将viewPager.setOffscreenPageLimit(0)设置为0,视图分页器仍将保留-1和+1片段。 - Nilesh Deokar
我已经编辑了我的答案,因为setOffscreenPageLimit()可以处理的最小和默认离屏片段是一个。Fragment.setUserVisibleHint()是最好的解决方案。 - Seif E. Attia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接