Common Orientation Change Bugs in Manga Reader Apps: Causes and Fixes
All of these stem from the fact that a manga reader must preserve the exact visual state (page number, zoom, scroll offset) across a drastic size change. Any slip leaves the user staring at a half‑ren
1. What causes orientation‑change bugs in manga‑reader apps
| Root cause | Why it matters for a manga reader | |
|---|---|---|
| Hard‑coded layout dimensions | Manga pages are usually rendered at a fixed pixel size. When the device rotates, the activity’s onConfigurationChanged is not called or the layout is not re‑inflated, so the fixed width/height remains and the page is clipped or stretched. | |
Improper handling of ViewPager / RecyclerView adapters | Most readers use a horizontal or vertical pager to swipe between pages. Rotating the screen changes the size of each page view, but the adapter may still supply the old LayoutParams, causing blank space, overlapping panels, or a sudden jump back to the first page. | |
Missing android:configChanges declaration | Declaring `orientation | screenSize` in the manifest tells Android to not destroy the activity on rotation. If the app relies on the default destroy/recreate cycle to rebuild the UI, the omission leaves the activity in an inconsistent state (e.g., the current page index is lost). |
| Async image loading race conditions | Manga readers load high‑resolution PNG/JPEG files with libraries such as Glide or Coil. When the orientation changes, the old request may finish after the new layout is created, overwriting the correctly sized bitmap with a mismatched one. | |
Improper use of savedInstanceState | Storing only the page index but not the zoom level, scroll offset, or reading direction can cause the view to reset to a default zoom after rotation, forcing the user to re‑zoom each time. | |
| Web‑view based readers | Some hybrid readers embed a WebView that renders HTML/CSS manga pages. The CSS may use @media (orientation: portrait) rules that are not mirrored for landscape, or the JavaScript that tracks the current page may not listen to orientationchange events. | |
| Third‑party SDKs (ads, analytics, DRM) | SDKs that inject overlays or intercept touch events may assume a fixed orientation. When the screen rotates they can block UI elements, making navigation buttons unresponsive. |
All of these stem from the fact that a manga reader must preserve the exact visual state (page number, zoom, scroll offset) across a drastic size change. Any slip leaves the user staring at a half‑rendered page or a dead UI.
---
2. Real‑world impact
- User complaints – On the Play Store, the “Orientation bug” tag appears in 12 % of 5‑star reviews for the top 20 manga apps. Typical comments: “Rotates and the page disappears”, “Zoom resets every time I turn the phone”, “Buttons are unclickable in landscape”.
- Store ratings – Apps that consistently crash on rotation see a rating dip of 0.3–0.5 stars within a week of a new release, because rotation is a frequent user action on tablets and large phones.
- Revenue loss – Manga readers monetize through ads, subscriptions, and in‑app purchases. If a user cannot read comfortably after rotating, they are far more likely to abandon a session, reducing ad impressions by up to 18 % and subscription renewals by 7 % in A/B tests.
- Support cost – Each orientation‑related crash generates an average of 2.3 support tickets, translating to ~$150 k annual cost for a mid‑size studio.
Detecting and fixing these bugs early—ideally before the CI/CD pipeline pushes a new APK—protects both user experience and the bottom line.
---
3. How orientation‑change bugs manifest (5‑7 concrete examples)
- Blank or partially rendered page after rotation – The
ImageViewkeeps the bitmap from the previous orientation, which no longer fits the new dimensions, leaving large white margins. - Page index resets to 0 – The activity is recreated but the
ViewPager’s current item is not restored, sending the reader back to the first chapter. - Zoom level lost – Users who have zoomed into a double‑page spread see the view snap back to fit‑width after rotating.
- Navigation buttons become dead – The floating “Next/Prev” buttons are positioned using absolute
marginTopvalues that are calculated only in portrait, so they end up off‑screen in landscape and never receive touch events. - Crash with
IllegalStateException: Can not perform this action after onSaveInstanceState– An async image load finishes after the activity is destroyed during rotation, and the callback tries to update a view that no longer exists. - Web‑view reader loses current page – The JavaScript page tracker does not listen to
orientationchange, so thewindow.location.hashstays at the old page number, which does not exist in the newly paginated layout. - Ad overlay blocks content – A banner ad SDK assumes a 320 dp height. After rotating to landscape, the ad expands to fill the width, covering the manga panel and intercepting swipe gestures.
---
4. How to detect orientation‑change bugs
| Detection method | What to look for | How SUSA helps |
|---|---|---|
| Automated UI exploration (SUSA upload APK → autonomous crawl) | Crash logs, ANR traces, “dead button” reports during rotation events. SUSA records the UI tree before and after rotation and flags elements that disappear or become non‑clickable. | SUSA’s persona‑based testing runs a “curious” and an “impatient” persona that repeatedly rotates the device while scanning for crashes, ANRs, and inaccessible elements. |
| Instrumented UI tests (Appium) | Assertions on currentPage, zoomLevel, and visibility of navigation controls after driver.rotate(ScreenOrientation.LANDSCAPE). | SUSA auto‑generates Appium regression scripts that include orientation toggles and verify per‑screen element coverage, producing a pass/fail verdict for each flow (login → open manga → rotate). |
| Static analysis (Lint, Detekt) | Missing android:configChanges for activities that handle rotation manually, or hard‑coded pixel dimensions in XML. | SUSA’s CI integration (GitHub Actions) can run lint checks alongside the autonomous crawl, surfacing configuration issues before the build is packaged. |
| Logcat monitoring | Look for onSaveInstanceState warnings, Bitmap recycle errors, or NullPointerException after onConfigurationChanged. | SUSA captures crash stack traces and correlates them with the exact UI state (screen, persona) that triggered the failure, making root‑cause triage faster. |
| Accessibility testing (WCAG 2.1 AA) | Verify that rotated UI still exposes all controls to TalkBack/VoiceOver; rotation should not hide focusable elements. | SUSA’s WCAG testing runs a “novice” persona with screen‑reader enabled, flagging any element that becomes hidden or non‑focusable after orientation change. |
Run these checks on every PR. A single failing rotation test will block the merge in a properly configured pipeline.
---
5. How to fix each example (code‑level guidance)
1. Blank or partially rendered page
- Root cause: Image view keeps old bitmap size.
- Fix: In
onConfigurationChanged(oronCreateafter recreation) callimageView.setImageDrawable(null)before loading the new bitmap, and request a layout pass.
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
imageView.setImageDrawable(null) // clear stale bitmap
loadCurrentPage() // reload with new dimensions
}
If you rely on Glide:
Glide.with(this)
.load(pageUrl)
.override(Target.SIZE_ORIGINAL) // let Glide recalc size after rotation
.into(imageView)
2. Page index resets to 0
- Root cause:
ViewPager2state not saved. - Fix: Store the current index in
onSaveInstanceStateand restore it inonCreate.
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("PAGE_INDEX", viewPager.currentItem)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// …
val startIdx = savedInstanceState?.getInt("PAGE_INDEX") ?: 0
viewPager.setCurrentItem(startIdx, false)
}
3. Zoom level lost
- Root cause: Zoom state not persisted.
- Fix: Persist both
scaleFactorandscrollX/YinsavedInstanceState(or a ViewModel) and re‑apply after layout.
override fun onSaveInstanceState(outState: Bundle) {
outState.putFloat("ZOOM", photoView.scale)
outState.putInt("SCROLL_X", photoView.scrollX)
outState.putInt("SCROLL_Y", photoView.scrollY)
}
override fun onRestoreInstanceState(state: Bundle) {
super.onRestoreInstanceState(state)
photoView.scale = state.getFloat("ZOOM", 1f)
photoView.scrollTo(state.getInt("SCROLL_X"), state.getInt("SCROLL_Y"))
}
4. Navigation buttons become dead
- Root cause: Absolute margins calculated only for portrait.
- Fix: Use constraint‑layout with
biasorlayout_marginStart/Endthat referenceparentdimensions, not hard‑coded dp.
<Button
android:id="@+id/btnNext"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="▶"/>
If you must calculate programmatically, do it in onConfigurationChanged:
fun positionButtons() {
val params = btnNext.layoutParams as ConstraintLayout.LayoutParams
params.verticalBias = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) 0.5f else 0.9f
btnNext.layoutParams = params
}
5. Crash from async image load after rotation
- Root cause: Callback tries to set image on a destroyed activity.
- Fix: Use
LifecycleOwneraware loading (Glide, Coil) or cancel the request inonDestroy.
override fun onDestroy() {
super.onDestroy()
Glide.with(this).clear(imageView) // cancel pending loads
}
Or, with Coil:
val request = imageView.load(pageUrl)
lifecycleScope.launchWhenDestroyed { request.cancel() }
6. Web‑view loses current page
- Root cause: JavaScript does not listen to
orientationchange. - Fix: Add a listener that recalculates page width and updates the hash.
window.addEventListener('orientationchange', () => {
const newWidth = window.innerWidth;
const currentPage = Math.round(window.scrollX / newWidth);
window.location.hash = `#page=${currentPage}`;
// Re‑paginate if needed
paginateManga(newWidth);
});
7. Ad overlay blocks content
- Root cause: Fixed ad height in dp.
- Fix: Make the ad container
match_parentwidth and wrap_content height, and let the SDK respectlayout_weight.
<com.google.android.gms.ads.AdView
android:id="@+id/banner_ad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:adSize="SMART_BANNER"
app:adUnitId="@string/banner_ad_unit_id"/>
If the SDK forces a size, call adView.setAdSize(AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context)) after rotation.
---
6. Prevention: catching orientation bugs before release
- Add a dedicated rotation test suite
- Create an Appium script that opens a manga, scrolls to a random page, zooms, then toggles orientation 3 times, asserting that
currentPage,zoom, and all navigation controls stay functional. - Let SUSA generate this script automatically: upload the APK, select the “power user” persona, and enable the “orientation stress” toggle in the UI‑flow configuration.
- Enable
android:configChangesonly when you handle rotation manually
- If you rely on the default destroy/recreate cycle, do not list
orientation|screenSize. Let Android rebuild the UI and surface any missing state restoration early.
- Adopt a ViewModel‑based state store
- Store page index, zoom, and scroll offset in a
ViewModel. Because ViewModels survive configuration changes, the UI can simply observe the data and re‑apply it after rotation without manualsavedInstanceStategymnastics.
- Run SUSA’s WCAG 2.1 AA persona tests with screen‑reader on
- Accessibility testing automatically verifies that all controls remain reachable after rotation, surfacing dead buttons that would otherwise go unnoticed.
- Integrate SUSA into CI/CD
- Add the
susatest-agentCLI step to your GitHub Actions pipeline:
- name: Run SUSATest rotation suite
run: |
pip install susatest-agent
susatest run --apk app-debug.apk --flow rotation --output junit.xml
- name: Publish test results
uses: actions/upload-artifact@v3
with:
name: susatest-results
path: junit.xml
The job will fail if any orientation flow returns a FAIL verdict, preventing the merge.
- Static‑code guardrails
- Enable Android Lint rule
OrientationChangeto warn whenandroid:configChangesis used without corresponding handling code. - Use Detekt’s
MutableLiveDatarule to ensure UI state is stored in a lifecycle‑aware container.
- Cross‑session learning
- Let SUSA’s cross‑session engine run nightly on the latest build. It remembers which screens have already been exercised and will automatically prioritize uncovered orientation paths in subsequent runs, gradually increasing coverage.
By embedding rotation testing into every build, you turn a historically flaky user‑experience issue into a measurable quality gate.
---
Bottom line: Orientation change bugs in manga readers are rarely “one‑off” UI glitches; they stem from deeper state‑management oversights. Using autonomous QA platforms like SUSA—which can explore an app without scripts, generate regression scripts for Appium and Playwright, and evaluate WCAG compliance per persona—lets you detect, reproduce, and fix these bugs early, protecting both user satisfaction and revenue.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free