Repository navigation
Releases
2.2.0
Added
smoothScrollToPosition(int), on all three views, animates the cell holding
the position toparchment_snapPositionthe way a fling gets there, where
setSelectionjumps. It runs at RecyclerView's programmatic speed, 25 ms
per inch, four times a snap's: a cell already laid out is landed on with one
decelerating scroll of exactly its snap distance; a cell that is not is
sought at a constant speed by the distance extrapolated from the edge cell's
size plus a viewport of runway, handed to the landing in the frame it is
laid out, and sought again from wherever it is whenever less than a viewport
of runway is left, so the motion stays even when the cells between were
larger than the estimate assumed. The landing is not capped at half a
second, so a long scroll decelerates from the seek's speed rather than
snapping the rest of the way. It obeys every bound a fling obeys:
android:padding*,parchment_scrollWithinContent, under which a cell the
bound holds short of its snap point decelerates onto the bound and rests
there, andparchment_isCircularScroll, under which it takes the shorter
way round. Aparchment_isViewPagerview is crossed page by page rather
than held to one, which needed the page clamp to learn that an animation is
not a gesture. It reportssettlingthenidleto the scroll listener, and
underparchment_selectOnSnapselects the cell at the snap position when it
comes to rest, once. The position is clamped to the adapter; before the first
layout, on an empty adapter, or when the cell is already at its snap point
nothing happens; it is ignored while a finger is down, and a touch cancels it
like a fling. UnderonScreenit scrolls the minimum that shows the cell
whole. A position asked for before the first layout is not remembered; a
pending target is a follow-up (#54, asked for in #19).
Changed
- A jump ends a running animation.
setSelectionand a data set change now
stop whatever was moving the content, a fling included, in the layout pass
the jump asks for, snapping whatever residual the jump left, so the view
rests with the incoming cell at its snap point and reports oneidle; the
fling used to carry on from the jumped offset. At rest nothing changes: no
frame runs and nothing is reported. - Breaking:
LayoutManagergains an abstractgetCellBreadth, a cell's
extent across the scroll axis, andmeasureBreadth, which
AbstractAdapterView.onMeasureasks for the view's size across it;
ScrollDirectionManagergains the cross-axis spec, padding and width/height
mappings that serve them. All are implementation details the views build and
call for themselves, so nothing outside the library should be subclassing or
calling them.
Fixed
parchment_selectOnSnapreported the selection a stop made only on the next
layout pass, so a fling that ended exactly on a snap point, with no residual
snap to run a further frame, selected the cell but did not tell the
OnItemSelectedListeneruntil something else laid the view out. A frame
that ends at rest now reports the selection its stop made, which is what
everysmoothScrollToPositionlanding needs
(aFlingThatEndsOnASnapPoint_withSelectOnSnap_reportsTheSelectionWithoutALayoutPass).
Two consequences to know about: underparchment_selectOnSnapa data set
change at rest that keeps or grows the count now reports its unchanged
selection one layout pass earlier, the same cell in the same place, once;
and a jump that lands mid-animation reports its selection where the cell is
at the jump, after which the residual snap moves it the few pixels to its
snap point, as it already did on main with the fling still running. Under
parchment_isCircularScrollwithparchment_selectOnSnap, a tap on a cell
reports that cell and then the cell nearest the start edge, because the
stop's snap selects the nearest cell to theonScreenposition circular
scrolling forces; main did the same silently, and this is now reported
rather than fixed.- A fling, snap or smooth scroll that ran to rest within its first frame, as a
short snap does and as any animation does when the main thread stalls for a
frame, reported its displacement but no scroll state: the dispatcher reports
a change at the end of a frame and the frame ended where it began, at
idle. It now reports the state the frame ran in before the state it ended
in, when the frame moved the content, so such a frame reportssettling
thenidleafter itsonScrolledand a frame that moved nothing still
reports nothing
(aSmoothScrollThatEndsInItsFirstFrame_stillReportsSettlingThenIdle,
aFlingThatEndsInItsFirstFrame_stillReportsSettlingThenIdle). android:layout_height="wrap_content"on a horizontal view and
android:layout_width="wrap_content"on a vertical one used to take every
pixel the parent had left, pushing the next sibling of aLinearLayoutoff
the screen:onMeasureset the measured size to the spec size whatever the
spec's mode, soAT_MOSTwas treated asEXACTLY. Across the scroll axis a
ListViewnow measures to its largest cell and aGridViewto its largest
row or column, the spacing between the views of the group included, plus
android:padding*, capped at the size offered underAT_MOSTand uncapped
underUNSPECIFIED.EXACTLYis untouched, somatch_parentand a fixed
size lay out exactly as before. Along the scroll axiswrap_contentstill
fills the parent, since wrapping there would mean measuring every item in the
adapter, andGridPatternView, whose cells are sized from the view, still
fills it on both axes. The size comes from the cells present at measure time:
the cells laid out, or before the first layout the cells that fill the
viewport from the start position, obtained through the recycler and handed
back to it the way the platformListViewmeasures underAT_MOST. That
first estimate cannot see a cell the layout back-fills before the start
position, one the over-scroll correction pulls in, or one a scroll reveals
later, so a layout that draws a cell larger than the size it was measured to
asks the view for one more layout, and the next measure, which reads the
drawn cells, grows the view to it. Cells of varying size across the scroll
axis are therefore better served by a fixed size, because the view resizes
as they come into view (#59, the remaining half of #10).- A horizontal
ListViewgave its children the mode of its own width spec as
their height mode, and aGridViewdid the same for the views it obtains for
a jump. They now take the mode of the height spec. Underwrap_contentthat
is what lets amatch_parentchild measure to its content rather than to
the whole height the parent offered; under a bounded width and an exact
height, a horizontal list inside aHorizontalScrollViewor a
wrap_content-wide parent, amatch_parentchild is now given the exact
height instead of a bound it measured itself to zero against. UnderEXACTLY
on both axes nothing changes.
2.1.0
Added
parchment_scrollWithinContent, on all three views, keeps the content inside
the view: the first cell's start never moves inside the view's start edge and
the last cell's end never moves inside its end edge, the bound thatonScreen
has always applied. Withstartthe last cell now stops with its end at the
view's end instead of being dragged to the start edge with empty space after
it,endis the mirror, andcenterholds both ends; content shorter than
the view does not scroll and sits at the snap position, at the start, at the
end or centred. The content rests either on a cell at the snap position or at
one of the two ends, whichever is nearer, so a drag, fling, page, tap or
setSelectionthat runs into an end stops there and asks for nothing further,
whether or not the cells divide the view evenly; a cell larger than the view is the exception,
and rests at its snap point as it does underonScreen. At an end,
parchment_selectOnSnapselects the cell that would have snapped there: the
nearest of the cells the bound holds short of their snap point. A page is
measured from where the anchor cell would snap without the bound, so paging
back from an end returns to the page the gesture came from. Content that fits
the view is put in place by the same correction on the first layout, so with
endorcenterthat first layout already selects, asonScreenhas always
done for short content.android:padding*bounds the content the way it
bounds a snap. The attribute is off by default and is opt-in because the old
behaviour is deliberate: letting every cell reach the snap point, the last and
first included, is what letsparchment_selectOnSnapselect every cell, and a
layout that relies on that must not change under it. It does nothing under
onScreen, which already keeps the content inside the view, or under
parchment_isCircularScroll, which has no ends (#21).
Changed
- Breaking: the
LayoutManagerAttributes,GridLayoutManagerAttributesand
GridPatternLayoutManagerAttributesconstructors take the new flag, after the
snap position;SnapPositionInterface.getSnapToPixelDistancetakes the drawn
cells and the interface gainsgetCellSettleDistanceand
getUnboundedSnapToPixelDistance. All are implementation details the views
build and call for themselves, so nothing outside the library should be
constructing or implementing them.
Fixed
- A drag that pushes the content past its end used to hold the last cell at the
snap position and then move the content one cell back under the finger,
reporting a settling state and firing a second selection for
parchment_selectOnSnap. The clamp stopped the animation from inside the
frame, before the correction had been laid out, so the snap that a stop hands
off to measured the cells where they had been rather than where they were. The
stop and the selection now run once the frame is laid out: the content stays
held on the last cell, reports no settling state, and selects it once. The
cell selected at an end is the cell nearest the snap position among those laid
out, which withoutparchment_scrollWithinContentis the held cell as before,
and underonScreen, whose every visible cell is at its snap position, is
still the held cell.
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.
...