Repository navigation
2.0.0
The first work on Parchment since 2014. Apart from the new cell divider,
behaviour of the views is unchanged; everything around them is new.
Published to Maven Central as mobi.parchment:parchment:2.0.0, the same
coordinates as 1.x.
Upgrading from 1.6.x
- Depend on
mobi.parchment:parchment:2.0.0; the artifact is an AAR now,
not anapklib. Minimum SDK is 21. - Every Parchment XML attribute gained a
parchment_prefix:
orientationisparchment_orientation,cellSpacingis
parchment_cellSpacing, and so on for all of them. Declare the namespace
asxmlns:parchment="http://schemas.android.com/apk/res-auto". The
old names are not kept as aliases, so a layout that still uses one fails
to build rather than silently ignoring it. The README lists every
attribute. - Class names, packages and the public Java API are unchanged. Only two
protectedhooks changed shape,createAdapterViewInitializerand the
constructors ofAdapterAnimatorandChildTouchGestureListener; a
subclass that overrides or calls them has to pass the new parameters on.
Highlights
setOnScrollListeneron every view, shaped like
RecyclerView.OnScrollListener, with the state and the displacement.parchment_dividerandparchment_dividerSizedraw a divider between
cells, in every view.parchment_viewPagerIntervalpages a fixed number of cells instead of a
viewport.- A snapping view comes to rest after a gesture instead of animating
forever, and animation frames no longer re-lay-out the whole ancestor tree. - The sample app is a playground that reaches every attribute and prints
the XML that reproduces what is on screen.
Everything below is the full record.
Changed
-
Breaking:
AbstractAdapterView.createAdapterViewInitializertakes two
more parameters, the divider drawable and its size. It isprotectedand
an implementation detail, but a subclass that overrides it — the pattern
the in-tree test views use to reach the gesture listener — has to take the
two new parameters and pass them on. -
The
GridPatternViewsample screen and the wrapping-heightGridView
sample screen draw a divider, so the feature is visible in the app. A
colour has no intrinsic size, so both declare an explicit
parchment_dividerSize. -
The sample's four fixed screens are replaced by a playground that reaches
every attribute and every combination of them. The first page lists the
high-level items: the three views, and the behaviours that cut across them
(paging, snapping, circular scrolling, dividers, selection, the scroll
listener). Each opens the playground pre-filled with a preset that shows
that item off — the four old screens are four of the presets — where every
parchment_*attribute is a control, plus theGridPatternViewpattern.
Every control carries a hint saying what its attribute does, and one whose
attribute would have no effect under the other settings disables itself
and says what it needs; the printed XML leaves such an attribute out too.
Show inflates the configured view with a status line of what
setOnScrollListener,OnItemSelectedListenerandOnItemClickListener
report, and an info button shows the layout that reproduces it, which Copy
puts on the clipboard. Parchment reads its attributes only when a view is
inflated and has no setters for them, so the playground layouts reference
?attr/playground_*theme attributes andPlaygroundThemeapplies one
overlay style per chosen option to the theme it inflates under. The README
had theGridPatternItemDefinitionparameters as
(left, top, width, height); the constructor takes
(top, left, height, width), and its example, which tiles either way, is
unchanged. -
Breaking: the
AdapterAnimatorandChildTouchGestureListener
constructors take aScrollListenerDispatcher. Both classes are
implementation details that the views build for themselves — the in-tree
test views reach the gesture listener by overriding
createAdapterViewInitializer, not by constructing one — so nothing outside
the library should be calling these constructors. The animator needs the
dispatcher because it owns the state machine: a state change that reaches no
frame, as when a drag is released exactly on the snap position and no
animation follows, would otherwise never be reported, and asking the
dispatcher there is what keeps a view with no listener from scheduling a
frame it does not need. -
GridViewrows report their bounds without allocating.Group.getTop,
getBottom,getLeftandgetRightwalked their views with an iterator
and a boxedIntegeraccumulator; they now index the list and accumulate
anint, because the divider pass reads a cell's bounds on every frame
while the view moves. An empty group reported those bounds by throwing a
NullPointerExceptionand now reports 0, matchinggetMeasuredWidthand
getMeasuredHeight, which already did. -
Breaking: every custom XML attribute now carries a
parchment_
prefix:orientationisparchment_orientation,cellSpacingis
parchment_cellSpacing, and so on for all eleven. Custom attribute names
share one flat namespace across everything the resource merger sees, so
Parchment's unqualified names collided with any other library declaring
the same name and the consumer could not build at all; aapt2 stops the
merge withduplicate value for resource 'attr/orientation' with config ''. Two declarations survive that merge only when they match exactly, so
cellSpacingas a dimension here and an integer there was already fatal,
and an attribute carryingenumorflagchildren —orientation,
snapPosition,gravity— collided even when both declarations were
byte-for-byte identical. Format, meaning, and the enum and flag values
are unchanged, and the old names are not kept as deprecated aliases:
2.0 is the breaking window and reading both would double the parsing
code. The README tables list them all. Substituting your own
res-auto prefix forparchmentbelow — neverandroid, whose own
orientationandgravityare a different namespace and must not be
touched — this is the whole migration:sed -i -E 's/\bparchment:(orientation|cellSpacing|isCircularScroll|snapToPosition|selectOnSnap|isViewPager|snapPosition|selectWhileScrolling|numberOfViewsPerCell|gravity|ratio)=/parchment:parchment_\1=/g' $(git ls-files '*.xml')The
declare-styleablenames (ListView,GridView,GridPatternView)
are deliberately unchanged: the resource merger unions same-named
styleables instead of failing them, and each app relinks the library's R
indices, so a consumer declaring its ownListViewstyleable still
builds. -
Sample: each photo is requested from the Unsplash resizer at the size it
will be shown at, instead of a fixed 800px wide image for every cell. The
size is a pair of dimension resources per sample screen, so the existing
-portandsw###dpqualifiers pick it the same way they already pick the
cell sizes themselves; the three that a layout dimension already states
alias it rather than repeating the number. Requests are in device pixels,
so density is applied once bygetDimensionPixelSize. -
Sample: Picasso 2.8 (was 2.71828). Same okhttp, but it depends on
androidx.exifinterfaceinstead ofcom.android.support:exifinterfaceand
support-annotations, which were the lastcom.android.supportartifacts
in the build. -
Sample: Picasso is built with a memory cache of a third of the heap instead
of the seventh it sizes itself at. A photo requested at the size of the view
is several times larger than the fixed 800px one it replaces, and on a
256MB-heap device only three GridPatternView cells fit in the default cache,
so scrolling back evicted almost everything: 4 hits against 104 misses over
a pass through the pattern view and back. A third of the heap, with the
pattern view's request trimmed, makes that 24 hits against 72. -
With
parchment_snapToPositionon, a fling ends on the nearest snap position
instead of decelerating to a stop and then starting a separate snap. The
layout manager computes the adjustment when the fling starts, extrapolating
with the edge cell's size when the end lies beyond the visible cells, and
the fling's end point is moved there while its physics stay the same. -
Snap, page, tap-to-snap, and programmatic scrolls use a decelerate curve
with a duration proportional to the distance (100 ms per inch, stretched
for the deceleration, capped at 500 ms), the same numbers RecyclerView's
snap helper uses. The old fixed 500 ms viscous-fluid curve covered 97% of
the distance in its first half and crawled the last few pixels one at a
time over the second half, which read as stutter at the end of a scroll.
Fling physics are unchanged. -
Animation frames (fling, snap, page, drag) run the layout step directly
from apostOnAnimationcallback and invalidate, instead of posting a
requestLayout()that re-measured and re-laid out the whole ancestor
tree every frame. A full layout pass still happens whenever the framework
asks for one, and a frame that fires while one is pending yields to it. -
Breaking: minimum SDK is 21 (was 8) and the library is compiled with
Java 17 against SDK 37. -
Breaking: the library is an AAR built by Gradle. The Maven
apklib
packaging, thepom.xmlfiles, and the Eclipse project files are gone. -
Custom attributes are declared through the
res-autonamespace
(xmlns:parchment="http://schemas.android.com/apk/res-auto"). -
Build: Gradle 9.7.1 with a checksummed wrapper, AGP 9.4.0, a version
catalog, configuration cache, and a JDK 21 daemon toolchain provisioned
by Gradle. -
Tests: Robolectric 4.16.1, AssertJ 3.27, androidx.test 1.7; the old
integrationMaven module lives inlibrary/src/testnow. -
Sample: targets SDK 37, Material theme, RTL-aware padding, Picasso 2.71828.
-
parchment_isViewPager="true"still advances a whole viewport of cells per
gesture by default, and it is now measured rather than summed. A page runs
from the cell nearest the snap position to the first cell that does not fit
the viewport whole, in each direction separately, so cells of different sizes
page by their own sizes, the landing point is a cell boundary even when the
gesture starts part-way through a cell, and a padded view counts only what
fits inside its padding. The room a page has is measured from where the
anchor cell settles, socenterandendsnapping page fewer cells than the
viewport would hold and no cell is skipped between pages. A cell larger than
the viewport is the first cell that does not fit, so it is still one page on
its own (fixed in 1.6.6) without a case of its own. Paging back is the mirror
of paging forward instead of reusing the forward distance, exactly for cells
still drawn and extrapolated from the first drawn cell's size for those
behind them. -
Sample: the demo photo set moved from imgur to the Unsplash CDN, and each
of the 21 photos now carries its own caption. Eighteen of them read
"National photo contest", which made paging and snapping hard to follow
because the caption did not change as the cell did. -
AbstractAdapterViewinvalidates the whole view after adding or removing
a child instead of the deprecatedinvalidate(Rect). -
The two ViewPager paging modes are a strategy per mode in
pageinterval/,
the way snapping has been one per mode insnapposition/:
CellCountPageIntervalandViewportPageIntervalbehind
PageIntervalInterface, picked once fromparchment_viewPagerIntervalby
PageIntervalSelectorand held onLayoutManager. They had been methods
onLayoutManagerwith no access modifier so that unit tests in the same
package could reach them. Every remaining declaration in the library was
given an explicitpublic,protectedorprivateat the same time, and
the unusedAnimation.setIdwas deleted rather than widened; none of them
was reachable from outside its package before, so no consumer loses a call.
Paging behaviour is unchanged.
Added
-
Releases are published to Maven Central by pushing a version tag. The
release.ymlworkflow checks the tag againstVERSION_NAME, runs the
fast CI gates, signs and uploads the AAR with its sources and Javadoc
through the Central Portal, and creates a GitHub Release whose notes are
this file's section for that version, with the AAR attached. -
setOnScrollListenerreports scrolling on all three views, which had no way
to observe it at all (#23, #19).OnScrollListenertakes the shape of
RecyclerView.OnScrollListenerrather thanAbsListView.OnScrollListener:
onScrollStateChangedwith aScrollStateofidle,draggingor
settling, andonScrolledwith this frame's movement, not the first visible
item and a visible count. The platformAdapterViewdeclares no
setOnScrollListenerof its own, so nothing is overridden or shadowed.
settlingcovers everything the content does on its own — a fling, a snap, a
tap-to-snap and a programmatic move are one perceived motion, and Parchment
hands a fling off to a snap without stopping in between, so splitting them
would report a state the user never sees. A state is reported only when it
changes, so a drag, release, snap and rest givesdragging,settling,
idleonce each, and a gesture inside the touch slop reports nothing.
Displacement is a single signed value along the scroll axis rather than a
(dx, dy)pair with one half always zero, because Parchment scrolls one axis
at a time; it is the displacement applied to the content, so its sign is the
opposite ofRecyclerView.onScrolled's, and the parameter is named
displacementrather thandxto say so. It is what the cells actually
moved, so a fling clamped at an end reports only the part that landed, a frame
that moves nothing reports nothing, and a jump —setSelection, a data set
change — reports no displacement because it is not a scroll. Both callbacks
run after the frame's layout, andonScrolledruns before the state change,
soidlemeans every movement has already been reported. What is reported is
a change and not a snapshot: a listener set while the content is already
moving is told the current state on the next frame when it differs from the
last state reported on that view, which is what keeps setting the same
listener twice mid-gesture from repeatingdragging. Setting a listener adds
no animation frames to a gesture, and the callback path allocates nothing: the
dispatcher reuses its two fields and the states are enum constants. -
parchment_dividerandparchment_dividerSizedraw a divider in all
three views, the thing the platformListViewhas and Parchment did not
(#25). A divider falls on every edge internal to the content and on none
at the content's outer boundary. Each drawn item is asked about its
trailing edge along each axis: where another item lies across the gap a
divider is painted there, spanning exactly the run the two items share
along the other axis, and an item with no neighbour on a side is at the
boundary and gets nothing. In aListViewthat is one divider between
each pair of items and one fewer than there are cells on screen. In
GridViewandGridPatternView, where a cell is a whole group, it is
also a divider between the items stacked inside one group, so a
GridPatternViewpattern of mixed spans and T-junctions is divided
without rows or columns having to exist. Every shared edge is painted
once. The divider is decoration and takes no space of its own: it is
centred in theparchment_cellSpacinggap, so the spacing is what you
size to make room for it, and a divider thicker than the spacing overflows
evenly onto both items and is painted over them — which is also what makes
one visible when the spacing is zero. Two items that overlap, as a negative
parchment_cellSpacinglays them, have no gap and get no divider. Where a
gap between rows crosses a gap between columns nothing abuts either gap, so
the crossing is left unpainted. A divider begins and ends where the items it
separates do, so
it stays insideandroid:padding*with them;android:clipToPaddingis
applied to the cells insideViewGroup.dispatchDrawand restored before
it returns, so it never reaches the divider under either setting. Without
parchment_dividerSizethe drawable's intrinsic size along the scroll
axis is used, and a colour has none, so a colour divider with no size
paints nothing, as with the platform widget. The divider is drawn as it is
given: drawable state and animation are not driven, as with the platform
widget. -
An instrumented harness,
ParchmentViewHarness, that inflates a view
from a layout with a realLayoutInflater, attaches it to an Activity at
an exact pixel size, drives real measure and layout passes, dispatches
real gestures, and hands a test an immutable snapshot of where the
children landed or of the pixels it painted. All fourteenparchment_
attributes are covered by on-device tests built on it, asserting geometry
and painted pixels rather than getters, as is paging in both modes: a real fling and a real slow drag each page by the
cells that fit the viewport, or byparchment_viewPagerIntervalcells, with
several cells on screen, with cells that do not divide the viewport, with
cells of unequal size, with a cell taller than the viewport, from a resting
position part-way through a cell, insideandroid:padding, in both
orientations, at both ends of the adapter, wrapping with
parchment_isCircularScroll, and onGridViewandGridPatternViewwhere
a cell is a whole group. -
Spotless (google-java-format, AOSP style) with SPDX license headers on
every Java file;javac -Xlint:all -Werror; Android Lint with warnings
as errors and no baseline. -
GitHub Actions: build + style + Robolectric tests + emulator-run
instrumented tests (android-ci.yml), security gates (security.yml:
action-pin lint, allowlist expiry, OSV-Scanner, dependency review,
gitleaks), and CodeQL (codeql.yml). Dependabot for Gradle and Actions. -
parchment_viewPagerInterval, on all three views, for how far one ViewPager
gesture pages. It carries an integer plus a named constant the way
layout_widthcarries a dimension plusmatch_parent:viewport, or the
literal0, or leaving the attribute out, keeps the viewport paging
Parchment has always done, whileNadvances exactly N cells from the cell
nearest the snap position, which is the carousel issue #26 asked for. Zero
is both the default and the value an absent attribute already yields, so
unset andviewportare one state and upgrading changes nothing until the
attribute is set. A negative interval is not a number of cells — it would
page backwards when the finger went forwards — so it is read asviewport
too. The attribute was named inLayoutManagerAttributessince 2014 and
carried as far as the layout manager, but nothing ever declared it in XML
or assigned it, so it always arrived as 0. -
An instrumented smoke test that inflates a
ListViewfrom XML on a real
framework. -
CONTRIBUTING.md,SECURITY.md,CODE_OF_CONDUCT.md,
docs/architecture.md, issue and PR templates, a pre-commit hook, and
scripts/ci_local.shmirroring CI.
Fixed
- A view with
snapToPositionon comes to rest after a gesture instead of
requesting animation frames forever. A snap asked the content to move to a
start that the draw limits, which are what stop a scroll, refuse to reach:
the clamp put the content back, the stop asked for the distance again, got
the same non-zero answer, and started another snap. Each snap position now
derives its snapped start, its two draw limits and its snap distance from
one formula, so the start a snap asks for is exactly the start the clamp
allows, in both directions. Two ways of reaching the old mismatch:- The snap distance was measured against the cell's representative view
while the draw limits were measured against the cell. Those agree only
while the representative is exactly as big as its cell, which holds for
ListView, whose cell is the view, and forGridView, whose
representative is the row's tallest view and so is what sets the row's
size, but not for aGridPatternViewpattern of more than one row, whose
representative is one grid row of it. This reachedcenterandend. centerbuilt its forward draw limit as(size + cellSize) / 2and its
target as(size - cellSize) / 2. Integer division truncates toward
zero, so the two rounded opposite ways once a cell was larger than the
viewport, and a cell larger by an odd number of pixels left a permanent
one-pixel error. This reached all three views.
- The snap distance was measured against the cell's representative view
- A
GridPatternViewno longer adds the view's start padding a second time
when placing each view inside its cell. The cell start already carries the
padding, so withandroid:paddingTopset (paddingLeftwhen scrolling
horizontally) the pattern was drawn that far past where the engine held the
cell, and a centred cell came to rest off centre by the padding. - A gesture whose first frame is held by the start or the end of the list no
longer counts the movement it was denied. The layout pass folded the
clamp's correction back into the distance a gesture has travelled only
while an animation continued, never on the frame that began it, so a first
frame carrying a drag the bounds refused left that drag in the running
total. In ViewPager mode the page that followed was then short by it in one
direction and long by it in the other: a backward swipe at the first cell
crept forward by the first frame's drag instead of holding still. Whether a
gesture's first frame carries a drag at all depends on whether a touch move
and an animation frame land in the same pass, which made it intermittent
and invisible to a unit test that drives layout by hand. - A fling, page, or programmatic scroll that ends hands off to its snap in
the same frame, after that frame's layout. The snap used to start one frame
later and was started twice, which left a frame with no motion at the
handoff. A layout pass while the view is at rest away from a snap position
still starts the snap, as before. - Animations no longer call
requestLayout()from inside the layout pass.
AdapterAnimatorasks anAnimationFrameScheduler(implemented by the
view) for the next frame, and the view posts a single coalesced request,
so starting a snap or fling while laying out no longer triggers the
framework's "requestLayout() improperly called during layout" second pass. AbstractAdapterView.onMeasurereports the size from the measure spec
instead of the raw spec (which leaked the spec mode into
getMeasuredState()), andonLayoutno longer runsMeasureSpec.getSize
on its layout coordinates, which laid out nothing when the view sat at a
negative offset inside its parent.- Snapping (after a drag or fling, on tap, on
setSelection, and after a
data set change) now measures against the size inside the view's padding,
the same size the layout pass uses. With padding set,centerandend
snaps used to target a point shifted by the padding, so the snap target
could disagree with where the content is allowed to stop. - Drag distance is accumulated between layout passes and consumed by the pass
that applies it. Two move events before one layout used to lose the first
delta, and an extra layout pass during a drag (an image loading into a
child, for instance) applied the last delta a second time. - The post-layout over-scroll check now reuses the draw-limit clamp in both
directions.centerandendsnap positions no longer stop a fling (and
start a snap) while the first or last cell is merely visible, a frame that
overshoots either end of the content is pulled back in the same frame
instead of one frame later, circular scrolling is never "corrected", and
the first visible cell index is no longer overwritten by the check (which
broke the scrollbar offset and refilling when scrolling back). - Restoring saved state into a freshly created view (rotation) kept the
first visible cell but lost the scroll offset inside it: the first layout
pass snapped back to the cell start. The restored offset now survives.
Removed
- Travis CI configuration, the
integrationmodule, lint baselines, and a
stray editor backup file.
Moved
- The repository now lives at
TinkerNorth/parchment; the old
EmirWeb/parchmentURL redirects.