package waymaker.top.android; // Copyright © 2015 Michael Allan. Licence MIT.
import android.content.res.*;
import android.graphics.drawable.VectorDrawable;
import android.util.*;
import android.widget.*;
import java.io.IOException;
import java.util.*;
import org.xmlpull.v1.XmlPullParserException;
import waymaker.gen.*;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import static waymaker.top.android.ForestVCalibrator.Calibration;
/**
A forest view oriented by the {@linkplain Wayranging#forester() forester}. Its main components
* are {@linkplain CountNode node} views (lettered). These it divides vertically between a peers viewer for
* showing the forester’s {@linkplain Forester#ascendTo(CountNode) leafward paths}, and a candidates viewer
* for showing its {@linkplain Forester#descend() rootward paths}.
*
* ◢◣ --- Up climber
* ———— --- Up pager for peers
* ti
* sh
* Peers --- rg
* qf ◄ --- Choice indicator
* pe
* ———— --- Down pager for peers
* ca --- Position in forest (forester.position)
* Candidates --- db
* ·· --- Ellipsis for omitted candidates
* fd
* ◥◤ --- Down climber
*
* --- Spaces
*
* The peers viewer alone is paged; the candidates viewer is ellipsed. The spaces pseudo-viewer is
* populated at the bottom with unused candidate spaces to achieve a rough, vertical centring that
* animates easily with LinearLayout.
*/
public @ThreadRestricted("app main") final class ForestV extends LinearLayout
{
/* * *
= constrain candidate addition
= constrain peer addition
- also using the constraint to guard and constrain call to enqueuePeersRequest
= reconstrain on calibration change
- direct call from calibrator
= allow for case of overconstrained parent in ForestVCalibrator
( deferred from above, for realistic test
- detected by squeezed height of down climber (bottom component) compared to up climber
- calibrated by (somehow) measuring height parent wants, compared with actual height available
- peer paging
( notebook 2015.12.14, 15
- long press to enable paging control
- to prevent accidental paging
/ - enabling is linked inversely to node specification
/ ( notebook 2015.12.16
/ - to prevent depaging of specific node
// then must do same for descent control
- rather: automatically despecify in both cases
- and respecify in back cases, provided nothing else specified meantime
- any other press will reveal a fading cue that explains the mechanism
*/
/** Constructs a ForestV.
*/
public @Warning("wr co-construct") ForestV( final Wayranging wr )
{
super( /*context*/wr );
pxActorMarkerWidth = Math.max( Math.round(wr.pxSP()), /*at least*/1 );
// Climber icons.
// - - - - - - - -
final VectorDrawable upClimberIcon;
final LinearLayout.LayoutParams climberLayoutParams;
{
// Make.
// - - - -
final Resources res = wr.getResources();
// try
// {
// final Class> c = Class.forName( "android.graphics.drawable.VectorDrawable" );
// final Method m = c.getMethod( "create", Resources.class, int.class );
// upClimberIcon = (VectorDrawable)m.invoke( null, res, R.drawable.top_android_forestv_climber );
// }
// catch( final Exception ex ) { throw new RuntimeException( ex ); }
//// call to API-hidden create method, but better to inline it for production code:
final XmlResourceParser p = res.getXml( R.drawable.top_android_forestv_climber );
try
{
// Advance to first element, else VectorDrawable throws XmlPullParserException
// "Binary XML file line #-1 tag requires viewportWidth > 0".
for( int t = p.getEventType(); t != START_TAG; t = p.next() )
{
if( t == END_DOCUMENT ) throw new XmlPullParserException( "Missing start tag" );
}
upClimberIcon = new VectorDrawable();
upClimberIcon.inflate( res, p, Xml.asAttributeSet(p) );
}
catch( final IOException|XmlPullParserException ex ) { throw new RuntimeException( ex ); }
final WaykitUI wk = WaykitUI.i();
final TypedValue tV = wk.typedValue();
if( wr.getTheme().resolveAttribute( android.R.attr.colorForeground, tV, /*resolveRefs*/true ))
{
upClimberIcon.setTint( tV.data );
}
// Scale.
// - - - -
final float aspectRatio =
(float)upClimberIcon.getIntrinsicWidth() / upClimberIcon.getIntrinsicHeight();
final int pxWidth = wk.px9mmExtendedWidth( Math.round( 35/*sp*/ * wr.pxSP() ));
// text sibling ∴ sp
final int pxHeight = Math.round( pxWidth / aspectRatio );
// upClimberIcon.setBounds( /*left*/0, /*top*/0, Android.right(0,pxWidth), Android.bottom(0,pxHeight) );
/// needn't scale icon itself, just the button:
climberLayoutParams = new LinearLayout.LayoutParams( pxWidth, pxHeight );
}
// / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / LAYOUT
setOrientation( VERTICAL );
setGravity( android.view.Gravity.BOTTOM );
TextView textV;
// Children at fixed indeces.
// - - - - - - - - - - - - - -
addView( upClimber = new Button(wr), climberLayoutParams );
upClimber.setBackground( upClimberIcon );
addView( textV = new TextView(wr) ); // up pager
textV.setText( "————" );
assert getChildCount() == C_TOP_PEER;
// Floating children, whose indeces depend on viewer population.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
addView( downPager = new TextView(wr) );
downPager.setText( "————" );
ellipsis = new TextView( wr );
ellipsis.setText( "··" );
addView( downClimber = new Button(wr), climberLayoutParams );
downClimber.setBackground( upClimberIcon );
downClimber.setRotation( 180f );
// changing floaters? maybe change c* indexing methods
// / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
calibrator = new ForestVCalibrator( this );
// Initialize population of spaces pseudo-viewer.
// - - - - - - - - - - - - - - - - - - - - - - - -
spaceLayoutParams_sync();
{
final int target = spaceCountTarget( calibration() );
if( target > 0 ) syncSpacesUp( target );
}
// Initialize population of node viewers. Replace population when forester replaces node cache.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
final Forester forester = wr.forester();
forester.bell().register( new Auditor()
{
{ populate( forester, forester.nodeCache() ); } // populate initially
private NodeCache nodeCache;
private void populate( final Forester forester, final NodeCache _nodeCache )
{
nodeCache = _nodeCache;
syncViewers( forester );
}
public void hear( Changed _ding ) // replaces population if forester has replaced node cache
{
final Forester forester = wr().forester();
final NodeCache _nodeCache = forester.nodeCache();
if( _nodeCache == nodeCache ) return;
depopulateNodeViewers();
populate( forester, _nodeCache );
}
}); // no need to unregister from wr co-construct
// Extend peers viewer when model extends.
// - - - - - - - - - - - - - - - - - - - - -
wr.forests().voterListingBell().register( new Auditor()
{
public void hear( Changed _ding )
{
final CountNode candidate = candidateViewed( wr().forester() );
final List extends CountNode> _peers = candidate.voters();
if( peerCount == _peers.size() ) return;
assert peerCount < _peers.size(); // forest.nodeCache guarantees no node is ever removed
syncPeersViewer( candidate, _peers );
}
}); // no need to unregister from wr co-construct
}
// - V i e w ----------------------------------------------------------------------------------------
protected @Override void onLayout( final boolean isChanged, final int left, final int top, final int right,
final int bottom )
{
super.onLayout( isChanged, left, top, right, bottom );
calibrator.activateMaybe(); /* Poll to detect whether a calibration parameter has changed.
Simple and robust, if only as a fallback. Execution may continue at this.reconstrain. */
}
//// P r i v a t e /////////////////////////////////////////////////////////////////////////////////////
private Calibration calibration() { return calibrator.calibration; }
// only default calibration is used, till ellipse is supported
private final ForestVCalibrator calibrator;
int candidateCount; // count of nodes in candidates viewer, comparable to forester.height
private CountNode candidateViewed( final Forester forester )
{
// topmost node of candidates viewer, or ground if viewer is grounded
return candidateCount > 0? getNodeChild(cTopCandidate()).node(): forester.nodeCache().ground();
}
private int cBottomCandidate() { return cCandidatesEndBound() - 1; }
// child index of bottom-most view in candidates viewer, assuming candidateCount > 0
private int cBottomPeer() { return C_TOP_PEER + peerCount - 1; }
// child index of bottom-most view in peers viewer, assuming peerCount > 0
private int cBottomSpace() { return cSpacesEndBound() - 1; }
// child index of bottom-most view in spaces pseudo-viewer, assuming spaceCount > 0
private int cCandidatesEndBound() { return cSpacesEndBound() - spaceCount; }
// child index of first view after candidates viewer
private int cSpacesEndBound() { return getChildCount(); }
// child index of first view after spaces pseudo-viewer; being none,
// returns end boundary of all children
int cTopCandidate() { return C_TOP_PEER + peerCount + 1; }
// child index of topmost view in candidates viewer, assuming candidateCount > 0
static final int C_TOP_PEER = 2; // child index of topmost view in peers viewer, assuming peerCount > 0
private void depopulateNodeViewers() // backward for faster removals
{
// Depopulate candidates.
// - - - - - - - - - - - -
if( candidateCount > 0 ) for( int c = cBottomCandidate();; --c )
{
removeCandidateToPool( c, getNodeChild(c) );
if( candidateCount == 0 ) break;
}
// Depopulate peers, cf. syncViewersOnDeficit.
// - - - - - - - - - - - - - - - - - - - - - - -
if( peerCount > 0 ) for( int c = cBottomPeer();; --c )
{
removePeerToPool( c, getNodeChild(c) );
if( peerCount == 0 ) break;
}
// Sync spaces.
// - - - - - - -
final int target = spaceCountTarget( calibration() );
if( spaceCount == target ) return;
if( spaceCount < target ) syncSpacesUp( target );
else syncSpacesDown( target );
}
private void depopulateViewers() // backward for faster removals
{
// Depopulate spaces pseudo-viewer.
// - - - - - - - - - - - - - - - - -
if( spaceCount > 0 ) syncSpacesDown( 0 );
// Depopulate other viewers.
// - - - - - - - - - - - - - -
depopulateNodeViewers();
}
final TextView downClimber;
private final TextView downPager;
final TextView ellipsis;
private NodeV getNodeChild( final int c ) { return (NodeV)getChildAt( c ); }
private Space getSpaceChild( final int c ) { return (Space)getChildAt( c ); } // hoping for Barbarella
int nodeCountForCalibration() { return peerCount + candidateCount + spaceCount; }
private final ArrayDeque nodePool = new ArrayDeque<>(); /* Pool because they're wr co-constructs.
∀ pooled node views, node model must be null to allow garbage collection. */
private void enpool( final NodeV nodeV )
{
assert nodeV.node() == null;
nodePool.add( nodeV );
}
private NodeV expoolNode() { return nodePool.remove(); }
int peerCount; // count of nodes in peers viewer, comparable to forester.position.voters.size
final int pxActorMarkerWidth;
void reconstrain() // called on calibration change
{
spaceLayoutParams_sync();
depopulateViewers(); // including spaces, whose dimensions may now be obsolete
syncViewers( wr().forester() );
}
private void removeCandidateToPool( final int cCandidate, final NodeV candidateV )
{
removeViewAt( cCandidate );
--candidateCount;
candidateV.node( null ); // release to garbage collector
enpool( candidateV );
}
private void removePeerToPool( final int cPeer, final NodeV peerV )
{
removeViewAt( cPeer );
--peerCount;
peerV.node( null ); // release to garbage collector
enpool( peerV );
}
private void removeSpaceToPool( final int cSpace, final Space space )
{
removeViewAt( cSpace );
--spaceCount;
enpool( space );
}
private final ServerCount serverCount = new ServerCount();
private int spaceCount; // count of nodes in spaces pseudo-viewer
private int spaceCountTarget( final Calibration cal )
{
int target = cal.candidateCount - candidateCount;
if( target < 0 ) target = 0; // likely impossible
return target;
}
private LayoutParams spaceLayoutParams;
private void spaceLayoutParams_sync()
{
int height = calibrator.nodeHeight;
if( height == ForestVCalibrator.NODE_HEIGHT_DEFAULT ) height = 0; // pending calibration
spaceLayoutParams = new LayoutParams( /*width*/0, height );
}
private final ArrayDeque spacePool = new ArrayDeque<>(); // pool for efficiency
private void enpool( final Space space ) { spacePool.add( space ); }
private Space expoolSpace() { return spacePool.remove(); }
/** Ensures the peers viewer is populated to match the forest model.
*
* @param candidate The viewed candidate.
* @param _peers The modelled peers, viz. candidate.voters.
*/
private void syncPeersViewer( final CountNode candidate, final List extends CountNode> _peers )
{
final int pN = _peers.size(); // modelled
if( pN > 0 ) // guarding against frequent, initial case of unextended voters
{
final Wayranging wr = wr();
final int pStart = peerCount; // viewed
int c = C_TOP_PEER;
int p = pN - 1; // starting at top peer, to speed each addition
assert _peers instanceof RandomAccess;
for(; p >= pStart; --p, ++c ) // close gap between viewed and modelled
{
final CountNode peer = _peers.get( p );
final NodeV peerV;
if( nodePool.size() > 0 )
{
peerV = expoolNode();
peerV.node( peer );
}
else peerV = new NodeV( this, peer );
addView( peerV, c );
++peerCount;
}
}
if( candidate.votersMaybeIncomplete() )
{
serverCount.enqueuePeersRequest( candidate.id(), wr().forester().forest(), /*paddedLimit*/0 );
}
}
private void syncSpacesDown( final int target )
{
assert spaceCount > target;
for( int c = cBottomSpace();; --c ) // backward for faster removals
{
removeSpaceToPool( c, getSpaceChild(c) );
if( spaceCount == target ) break;
}
}
private void syncSpacesUp( final int target )
{
assert spaceCount < target;
for( int c = cSpacesEndBound();; ++c )
{
final Space space = spacePool.size() > 0? expoolSpace(): new Space(getContext());
addView( space, c, spaceLayoutParams );
++spaceCount;
if( spaceCount == target ) break;
}
}
private void syncViewers( final Forester forester ) // ensures each populated to match forest and forester
{
final int _candidateCount = forester.height();
final CountNode _candidate = forester.position();
final int heightBalance = candidateCount - _candidateCount;
// View may be higher than model
// - - - - - - - - - - - - - - - -
if( heightBalance > 0 )
{
if( _candidateCount == 0 ) syncViewersOnSurplus();
else
{
final NodeV _candidateV = getNodeChild( cCandidatesEndBound() - _candidateCount );
// candidate view at model height
if( _candidateV.node() == _candidate ) syncViewersOnSurplus();
else syncViewersOnMismatch( _candidateCount, _candidate );
}
return;
}
final CountNode candidate = candidateViewed( forester );
// Or lower than model
// - - - - - - - - - - -
if( heightBalance < 0 )
{
CountNode _node = _candidate;
int b = heightBalance;
do // drop _node to view height
{
_node = _node.rootwardInPrecount().candidate();
++b;
}
while( b < 0 );
if( candidate == _node ) syncViewersOnDeficit( heightBalance, _candidate );
else syncViewersOnMismatch( _candidateCount, _candidate );
return;
}
// Else at same height.
// - - - - - - - - - - -
if( candidate == _candidate ) syncPeersViewer( candidate, candidate.voters() );
else syncViewersOnMismatch( _candidateCount, _candidate );
}
/** @param _candidate The new immediate candidate, viz. forester.position.
*/
private void syncViewersOnDeficit( final int heightBalance, final CountNode _candidate )
{
assert heightBalance < 0;
if( heightBalance == -1 ) // then a deficit of one candidate
{
// Depopulate peers viewer, cf. depopulateNodeViewers().
// - - - - - - - - - - - - - - - - - - - - - - - - - -
if( peerCount > 0 ) for( int c = cBottomPeer();; --c ) // backward for fast removals
{
final NodeV peerV = getNodeChild( c );
if( peerV.node() == _candidate )
{
// Moving new candidate to candidates viewer.
// - - - - - - - - - - - - - - - - - - - - - -
final int cDownPager = c + 1; // just below peerV
removeViewAt( cDownPager ); // lowering peerV in layout, a change that animates
addView( downPager, c ); // just above peerV, leaving peerV atop candidates viewer
--peerCount;
++candidateCount;
// Removing one space from spaces viewer.
// - - - - - - - - - - - - - - - - - - - -
final int cSpace = cBottomSpace();
removeSpaceToPool( cSpace, getSpaceChild(cSpace) );
}
else removePeerToPool( c, peerV );
if( peerCount == 0 ) break;
}
// Repopulate peers viewer.
// - - - - - - - - - - - - -
syncPeersViewer( _candidate, _candidate.voters() );
}
else // a deficit of multiple candidates, an abnormal case
{
throw new UnsupportedOperationException( "Not yet coded" );
}
}
/** @param _candidateCount The new candidate count, viz. forester.height.
* @param _candidate The new immediate candidate, viz. forester.position.
*/
private void syncViewersOnMismatch( final int _candidateCount, final CountNode _candidate )
{
// abnormal case, no need of finesse
depopulateNodeViewers();
syncViewersOnDeficit( -_candidateCount, _candidate );
}
private void syncViewersOnSurplus()
{
throw new UnsupportedOperationException( "Not yet coded" );
}
final Button upClimber;
Wayranging wr() { return (Wayranging)getContext(); }
}