IslamKhSh / CardSlider

Card Slider is an android component allows you to implement carousel effect with infinite indicators and more features
Apache License 2.0
334 stars 52 forks source link

Fatal Exception: java.lang.IndexOutOfBoundsException #26

Open TikTak123 opened 4 years ago

TikTak123 commented 4 years ago

The callback method onPageSelected receives a position greater than the list of elements.

CardSlider version: implementation 'com.github.IslamKhSh:CardSlider:1.0.1'

API level: Android 6, 9

Error log:

Fatal Exception: java.lang.IndexOutOfBoundsException: Index: 4, Size: 4
       at java.util.ArrayList.get(ArrayList.java:437)
       at com.example.ui.quiz.info.InfoViewModel$init$1.onPageSelected(InfoViewModel.java:47)
       at com.github.islamkhsh.viewpager2.CompositeOnPageChangeCallback.onPageSelected(CompositeOnPageChangeCallback.java:72)
       at com.github.islamkhsh.viewpager2.CompositeOnPageChangeCallback.onPageSelected(CompositeOnPageChangeCallback.java:72)
       at com.github.islamkhsh.viewpager2.ScrollEventAdapter.dispatchSelected(ScrollEventAdapter.java:386)
       at com.github.islamkhsh.viewpager2.ScrollEventAdapter.onScrolled(ScrollEventAdapter.java:176)
       at androidx.recyclerview.widget.RecyclerView.dispatchOnScrolled(RecyclerView.java:5173)
       at androidx.recyclerview.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:5338)
       at android.view.Choreographer$CallbackRecord.run(Choreographer.java:988)
       at android.view.Choreographer.doCallbacks(Choreographer.java:765)
       at android.view.Choreographer.doFrame(Choreographer.java:697)
       at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:967)
       at android.os.Handler.handleCallback(Handler.java:873)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at android.os.Looper.loop(Looper.java:214)
       at android.app.ActivityThread.main(ActivityThread.java:7156)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:975)

Sample code:

class PrizeAdapter1 @Inject constructor() : CardSliderAdapter<PrizeAdapter1.PrizeViewHolder>() {

    val items = ArrayList<Prize>()
    private val backgrounds = arrayListOf(
        R.drawable.frame_1,
        R.drawable.frame_2,
        R.drawable.frame_3,
        R.drawable.frame_3
    )

    override fun bindVH(holder: PrizeViewHolder, position: Int) {
        holder.bind(items[position], backgrounds[position % backgrounds.size])
    }

    override fun getItemCount(): Int = items.size

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PrizeViewHolder {
        return PrizeViewHolder(
            ViewholderQuizPrize1Binding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }

    class PrizeViewHolder(val binding: ViewholderQuizPrize1Binding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: Prize, background: Int) {
            binding.item = item
            binding.ivBackground.setImageResource(background)
        }
    }
}
@PerController
class InfoViewModel @Inject constructor() : BaseViewModel<InfoCallback>() {

    @Inject
    lateinit var mPrizesAdapter: PrizeAdapter1

    @Inject
    lateinit var mCategoriesAdapter: CategoryAdapter

    @Inject
    lateinit var mTopMembersAdapter: TopMemberAdapter

    @Inject
    lateinit var mHistoryAdapter: HistoryAdapter

    lateinit var onPrizeChangeListener: ViewPager2.OnPageChangeCallback
    val shouldShowHistory = ObservableBoolean(true)
    val havePointsText = ObservableField<String>()
    val fromPointsPerDayText = ObservableField<String>()
    val shouldShowContent = ObservableBoolean(false)
    var isDirty = false
    var mLoadGameStateDisposable: Disposable? = null

    override fun init(args: Bundle) {
        onPrizeChangeListener = object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                if (isDirty) {
                    val prize = mPrizesAdapter.items[position]

                    loadGameState(prize.index)

                    if (prize.index == 0 || prize.index == 1)
                        fromPointsPerDayText.set(
                            mContext.getString(
                                R.string.from_n_points_per_day,
                                mDecimalFormat.format(prize.maxScore)
                            )
                        )
                    else
                        fromPointsPerDayText.set(
                            mContext.getString(
                                R.string.out_of_n_available,
                                mDecimalFormat.format(prize.maxScore)
                            )
                        )
                }

                isDirty = true
            }
        }

        mCategoriesAdapter.listener = object : CategoryAdapter.Listener {
            override fun onScrollToPosition(position: Int) {
                mCallback.setCategoriesCurrentPosition(position)
            }

            override fun onPlayClick() {
                mCallback.openQuestions()
            }
        }

        mCallback.bindPrizes(mPrizesAdapter)
        mCallback.bindCategories(mCategoriesAdapter)
        mCallback.bindTopMembers(mTopMembersAdapter)
        mCallback.bindHistory(mHistoryAdapter)
    }

    override fun onAttach() {
        super.onAttach()
        loadQuizInfo(false)
    }

    @SuppressLint("CheckResult")
    fun loadQuizInfo(getNewData: Boolean) {
        mLoadGameStateDisposable?.dispose()

        mRepository.getQuizInfoAggregated(
            mPreferencesHelper.myPhoneNumber,
            mPreferencesHelper.subAccount,
            mPreferencesHelper.locale,
            getNewData
        )
            .compose(RxUtil.applyDefaults(this))
            .subscribe({
                handleQuizInfoResponse(it)
            }, {
                handleError(it)
            })
    }

    @SuppressLint("CheckResult")
    fun loadGameState(topType: Int) {
        mLoadGameStateDisposable?.dispose()

        mLoadGameStateDisposable = mNetworkHelper.getQuizGameState(
            mPreferencesHelper.myPhoneNumber,
            mPreferencesHelper.subAccount,
            topType
        )
            .compose(RxUtil.applySchedulers())
            .subscribe({ response ->
                mTopMembersAdapter.items.clear()
                mTopMembersAdapter.items.addAll(response.top)
                mTopMembersAdapter.notifyDataSetChanged()
            }, {
                handleError(it)
            })
    }

    private fun handleQuizInfoResponse(response: InfoAggregated) {
        shouldShowContent.set(true)
        havePointsText.set(
            mContext.getString(
                R.string.i_have_n_points,
                mDecimalFormat.format(response.gameStateResult.score)
            )
        )

        for (prize in response.prizeResult.prizes)
            if (prize.index == response.stateResult.gameLevel) {
                fromPointsPerDayText.set(
                    mContext.getString(
                        R.string.from_n_points_per_day,
                        mDecimalFormat.format(prize.maxScore)
                    )
                )
                break
            }

        mPrizesAdapter.items.clear()
        mPrizesAdapter.items.addAll(response.prizeResult.prizes)
        mPrizesAdapter.notifyDataSetChanged()
        mCallback.removeOnPrizeChangeListener(onPrizeChangeListener)
        mCallback.setPrizesCurrentPosition(0)
        mCallback.setOnPrizeChangeListener(onPrizeChangeListener)

        mCategoriesAdapter.items.clear()
        mCategoriesAdapter.items.addAll(response.levelResult.levels)
        mCategoriesAdapter.notifyDataSetChanged()
        mCallback.setCategoriesCurrentPosition(Integer.MAX_VALUE / 2)

        mTopMembersAdapter.items.clear()
        mTopMembersAdapter.items.addAll(response.gameStateResult.top)
        mTopMembersAdapter.notifyDataSetChanged()

        mHistoryAdapter.items.clear()
        mHistoryAdapter.items.addAll(response.gameStateResult.history)
        mHistoryAdapter.notifyDataSetChanged()
    }

    fun onMoreDetailsButtonClick() {
        mCallback.openParticipateRules()
    }

    fun onShowHistoryClick(toggle: Boolean) {
        shouldShowHistory.set(toggle)

        if (toggle)
            mCallback.scrollToBottom()
    }

    fun onCloseClick() {
        mCallback.closeController()
    }
}
interface InfoCallback : BaseViewModel.BaseCallback {
    fun openParticipateRules()
    fun bindPrizes(adapter: PrizeAdapter1)
    fun bindCategories(adapter: CategoryAdapter)
    fun bindTopMembers(adapter: TopMemberAdapter)
    fun bindHistory(adapter: HistoryAdapter)
    fun setOnPrizeChangeListener(listener: ViewPager2.OnPageChangeCallback)
    fun removeOnPrizeChangeListener(listener: ViewPager2.OnPageChangeCallback)
    fun setCategoriesCurrentPosition(position: Int)
    fun setPrizesCurrentPosition(position: Int)
    fun openQuestions()
    fun closeController()
    fun scrollToBottom()
}
class InfoController : BaseController(), InfoCallback {

    @Inject
    lateinit var mViewModel: InfoViewModel
    lateinit var binding: ControllerQuizInfoBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
        BaseApplication.getComponent().controllerComponent(ControllerModule(activity)).inject(this)
        binding = ControllerQuizInfoBinding.inflate(inflater, container, false)
        binding.viewmodel = mViewModel
        mViewModel.setCallback(this, args)
        binding.swipeLayout.setColorSchemeResources(R.color.colorPrimary, R.color.black)

        return binding.root
    }

    override fun openParticipateRules() {
        router.pushController(
            RouterTransaction.with(ParticipateRulesController())
                .pushChangeHandler(HorizontalChangeHandler())
                .popChangeHandler(HorizontalChangeHandler())
        )
    }

    override fun openQuestions() {
        router.pushController(
            RouterTransaction.with(QuestionController())
                .pushChangeHandler(HorizontalChangeHandler())
                .popChangeHandler(HorizontalChangeHandler())
        )
    }

    override fun bindPrizes(adapter: PrizeAdapter1) {
        binding.vpPrizes.adapter = adapter
    }

    override fun bindCategories(adapter: CategoryAdapter) {
        binding.vpCategories.adapter = adapter
    }

    override fun bindTopMembers(adapter: TopMemberAdapter) {
        binding.rvTopMembers.layoutManager = LinearLayoutManager(activity)
        binding.rvTopMembers.adapter = adapter
    }

    override fun bindHistory(adapter: HistoryAdapter) {
        binding.rvHistory.layoutManager = LinearLayoutManager(activity)
        binding.rvHistory.adapter = adapter
    }

    override fun setOnPrizeChangeListener(listener: ViewPager2.OnPageChangeCallback) {
        binding.vpPrizes.registerOnPageChangeCallback(listener)
    }

    override fun removeOnPrizeChangeListener(listener: ViewPager2.OnPageChangeCallback) {
        binding.vpPrizes.unregisterOnPageChangeCallback(listener)
    }

    override fun setCategoriesCurrentPosition(position: Int) {
        binding.vpCategories.currentItem = position
    }

    override fun setPrizesCurrentPosition(position: Int) {
        binding.vpPrizes.currentItem = position
    }

    override fun closeController() {
        router.handleBack()
    }

    override fun scrollToBottom() {
        binding.root.postDelayed({
            binding.nestedScrollView.fullScroll(View.FOCUS_DOWN)
        }, 30)
    }

    override fun onChangeStarted(
        changeHandler: ControllerChangeHandler,
        changeType: ControllerChangeType
    ) {
        super.onChangeStarted(changeHandler, changeType)
        if (!changeType.isEnter) {
            mViewModel.onDetach()
        } else {
            binding.root.postDelayed({
                mTabStateManager.setTab(TabStateManager.Tabs.QUIZ_INFO_SCREEN)
                mViewModel.onAttach()
            }, 30)
        }
    }

    override fun showMessage(message: String, vararg duration: Int) {
        showToast(binding.root, message)
    }

    override fun showMessage(message: Int, vararg duration: Int) {
        showToast(binding.root, message)
    }
}
TikTak123 commented 4 years ago

I was able to reproduce the error. Here is an example video where it is reproduced https://drive.google.com/file/d/1RK2yI_KOsl2Mdu3KpWu6vSKX417pNvov/view?usp=sharing I just added this peace of code to your sample project

viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                if (position > movies.size - 1)
                    throw IndexOutOfBoundsException()
            }
        })