Previously in this series we’ve dissected the app that I used for my presentation at AndroidConf Brasil 2011. In this part we’ll look at the final custom control. In order to understand what’s going on, you really need to have read the previous articles in this series as some of the concepts (such as slide phases) are specific to this application. In the previous article we introduced the concept of a View which can change state depending on the phase of its parent SlideLayout by means of the Phaseable interface. In this part we’ll look at a Phaseable implementation – specifically a TextView upon which we can change the visibility depending on the current view phase of the parent SlideLayout.
There’s a custom control named HighlightedTextView which is an extension of the HightlightedTextView that we developed in the series on Custom Controls. The concept of a TextView which can have custom styles applied to a section of its text is a concept that is explained in that series, so I won’t cover it in any depth here.
As with allof the custom controls that we’ve encountered thus far, HighlightedTextView has some custom attributes that we’ve declared in res/values/attrs.xml:
[xml][/xml]
The [show|hide|gone]Phase
attributes each contain a comma separated list of the phases in which this control will be visible, invisible, and gone respectively. The [show|hide]Animation
attributes define animations which will be executed when the visibility changes. The pattern
and highlightTextAppearance
contain a regex which determines the text to be highlighted, and the style to apply to that text.
If we have a look at the fields and constructors, this should all be fairly self explanatory:
[java] public class HighlightedTextView extends TextView implements Phaseable{
private Pattern pattern = null;
private int highlightStyle = 0;
private List
private List
private List
private Animation showAnimation = null;
private Animation hideAnimation = null;
public HighlightedTextView( Context context, AttributeSet attrs )
{
this( context, attrs, 0 );
}
public HighlightedTextView( Context context, AttributeSet attrs,
int defStyle )
{
super( context, attrs, defStyle );
TypedArray ta = context.obtainStyledAttributes( attrs,
R.styleable.HighlightedTextView );
CharSequence text = ta.getString(
R.styleable.HighlightedTextView_android_text );
if (text != null)
{
updateText( text );
}
int patternRes = ta.getResourceId(
R.styleable.HighlightedTextView_pattern, 0 );
String pattern = null;
if( patternRes > 0 )
{
pattern = context.getResources().getString(
patternRes );
}
else
{
pattern = ta.getString(
R.styleable.HighlightedTextView_pattern );
}
if (pattern != null)
{
setPattern( pattern );
}
int style = ta.getResourceId(
R.styleable.HighlightedTextView_highlightTextAppearance,
0 );
if (style >= 0)
{
setHighlightStyle( style );
}
populate(
ta.getString( R.styleable.HighlightedTextView_showPhase ),
showPhase );
populate(
ta.getString( R.styleable.HighlightedTextView_hidePhase ),
hidePhase );
populate(
ta.getString( R.styleable.HighlightedTextView_gonePhase ),
gonePhase );
int a = ta.getResourceId(
R.styleable.HighlightedTextView_showAnimation,
0 );
if (a > 0)
{
showAnimation = AnimationUtils.loadAnimation( context, a );
}
a = ta.getResourceId(
R.styleable.HighlightedTextView_hideAnimation, 0 );
if (a > 0)
{
hideAnimation = AnimationUtils.loadAnimation( context, a );
}
ta.recycle();
if( getFirst( showPhase ) != 0 && getVisibility() == VISIBLE )
{
setVisibility( INVISIBLE );
}
}
One thing worth noting is that for the pattern
field we first try to resolve to a resource ID and, if that fails, resolve to a static string entered in the XML layout. This matches format="string|reference"
that we declared for this attribute in attrs.xml and enables us to use either a static string or a reference to a resource ID for this attribute in our layout XML.
At the end of this method we do a check to determine the initial visibility of this View. However, it is good practise to set the initial visibility in the XML layout.
The only other thing here which should require an explanation is the use of a utility method named populate which converts a comma delimited list of integers in to a List
{
list.clear();
if (str != null && !str.isEmpty())
{
String[] tokens = str.split( “\\s?,\\s?” );
for (String token : tokens)
{
list.add( Integer.parseInt( token ) );
}
}
Collections.sort( list );
}
[/java]
This method will populate the supplied list with a list of comma-delimited integers in the supplied String. The list gets sorted into ascending order. This implementation contains no checking that the supplied string is, in fact, a comma-separated list of integer values, and it would certainly be worth adding such a check if you were to use this in production code.
Next we have another couple of utility methods which will return us the first and last int value in a given List
{
return list.isEmpty() ? 0 : list.get( list.size() – 1 );
}
private static int getFirst( List The next thing that we’ll look at is the two methods that we need to implement from the Phaseable interface: This is simply using the getLast method for each of our three phase lists to determine the maximum value from them all. The setPhase method seems a little more complex but is, in fact, relatively simple:
{
return list.isEmpty() ? 0 : list.get( 0 );
}
[/java]
{
return Math.max(
getLast( showPhase ),
Math.max(
getLast( hidePhase ),
getLast( gonePhase ) ) );
}
[/java]
{
if (showPhase.contains( phase ))
{
setVisibility( VISIBLE );
if (showAnimation != null)
{
startAnimation( showAnimation );
}
}
else
{
if (hidePhase.contains( phase ) || gonePhase.contains( phase ))
{
final int visibility = gonePhase.contains( phase ) ?
GONE : INVISIBLE;
if (hideAnimation != null)
{
hideAnimation.setAnimationListener( new AnimationListener()
{
public void onAnimationStart( Animation animation )
{
}
public void onAnimationRepeat( Animation animation )
{
}
public void onAnimationEnd( Animation animation )
{
setVisibility( visibility );
}
} );
startAnimation( hideAnimation );
}
else
{
setVisibility( visibility );
}
}
}
return getLast( showPhase ) <= phase && getLast( hidePhase ) <= phase && getLast( gonePhase ) <= phase; } [/java] The majority of this method is actually an AnimationListener which we need to use when applying an animation to a control where the visibility is changing to INVISIBLE or GONE. If we immediately set the visibility on these, then the visibility would change before the animation was run – which would effectively stop the animation from happening. Thus we use an AnimationListener upon which the onAnimationEnd will be called when the animation finishes. It is in this method that we set the visibility.
That concludes our look at the custom controls that comprise the Presenter app. In the next part of this series, we’ll have a look at the layouts that make up the slides within the presentation using these controls.
I apologise if the articles in this series so far have been a little dry and lacking any screen shots showing what happens on screen. This is because the custom controls that we have explored so far don’t actually do anything on their own, it is only when they are incorporated in to layouts that we can begin to see things happening. We’ll begin to see the fruits of our labour in terms of pretty screen shots in the next article, I promise!
The source code for PresenterLite can be found here.
© 2011, Mark Allison. All rights reserved.
Copyright © 2011 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.