tag:blogger.com,1999:blog-4268594322600124112024-02-19T07:56:44.705+03:00Android. Tips.Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.comBlogger15125tag:blogger.com,1999:blog-426859432260012411.post-38301099816277661022012-10-18T08:52:00.001+04:002012-10-18T08:52:21.535+04:00Tips code updated.<div dir="ltr" style="text-align: left;" trbidi="on">
The "Tips" project was updated. Deprecated methods were removed.<br />
SplashScreen activity settings were fixed in manifest. (Thanks to Eric Taix).<br />
Min SDK was set to 8, target SDK to 16.<br />
<br /></div>
Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-426859432260012411.post-77407592686668001362012-10-17T09:30:00.002+04:002012-10-18T08:53:21.342+04:00Carousel Jelly Bean issue.<div dir="ltr" style="text-align: left;" trbidi="on">
Carousel Jelly Bean issue was fixed.</div>
Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-426859432260012411.post-30433070348601633922012-01-11T10:56:00.000+04:002012-01-11T11:02:48.186+04:00Options menu like ActionSheet<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
<br />
<a href="https://code.google.com/p/android-tips-demo/"><i></i></a><br />
<div style="color: white; filter: Glow(color=#ff0000, strength=12); font-size: 12pt; width: 100%;">
<a href="https://code.google.com/p/android-tips-demo/"><i>Source Code</i></a></div>
<br />
<div style="color: white; filter: Glow(color=#ff0000, strength=12); font-size: 12pt; width: 100%;">
<a href="#"><i>Русский перевод. Pending...</i></a></div>
<br />
<br />
<br />
Now I'll show how to create options menu that will look
like iPhone's Action Sheet.<br />
<br />
<span class="Apple-style-span" style="font-size: large;">Preparings.</span><br />
<br />
<br />
First let's create animations for the menu. The menu will
show from the bottom of the device screen. So the "in" animation
will look like:<br />
<br />
<pre class="xml" name="code"><set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="100%"
android:toXDelta="100%"
android:fromYDelta="0%"
android:toYDelta="100%"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/linear_interpolator"
>
</translate>
</set>
</pre>
As you can see it's the "translate" animation. The animated view
will be always 100% screen width and will grow from 0% to 100%
it's height.
<br />
Vice versa the "out" we'll define as following:
<br />
<pre class="xml" name="code"><set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="100%"
android:toXDelta="100%"
android:fromYDelta="100%"
android:toYDelta="0%"
android:duration="@android:integer/config_shortAnimTime"
android:interpolator="@android:anim/linear_interpolator"
>
</translate>
</set>
</pre>
<br />
It will shrink from 100% to 0% height.
<br />
And to apply the animation to a view:
<br />
<pre class="xml" name="code"> <style name="Animations.MenuAnimation">
<item name="android:windowEnterAnimation">
@anim/menu_animation_in</item>
<item name="android:windowExitAnimation">
@anim/menu_animation_out</item>
</style>
</pre>
<br />
The menu items will be the same as in Dialog demo, but with gray colors.
And at last to complete XML stuff there are the menu item layout and the menu layout.
<br />
<pre class="xml" name="code"><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#224c4c4c"
android:gravity="center"
android:orientation="vertical"
android:padding="4dip" >
<Button
android:id="@+id/custom_menu_item_caption"
style="@style/button_black"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Add from URL" >
</Button>
</LinearLayout>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#00000000"
android:orientation="vertical"
android:padding="0dip" >
<View
android:layout_width="fill_parent"
android:layout_height="0.5dip"
android:background="#ff4f4f4f" />
<ImageView
android:layout_width="fill_parent"
android:layout_height="20dip"
android:gravity="center"
android:scaleType="fitXY"
android:src="@drawable/menu_header" />
<View
android:layout_width="fill_parent"
android:layout_height="0.5dip"
android:background="#ff4f4f4f" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#aa000000"
android:orientation="vertical"
android:padding="0dip" >
<TableLayout
android:id="@+id/custom_menu_table"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:stretchColumns="*" >
</TableLayout>
</LinearLayout>
</LinearLayout>
</pre>
<br />
Menu Header:
<br />
<pre class="xml" name="code"><shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="270"
android:type="linear"
android:endColor="#444c4c4c"
android:startColor="#44ffffff"
/>
</shape>
</pre>
<br />
<span class="Apple-style-span" style="font-size: large;">The code</span>
<br />
The code is quite clear. On base of the PopupWindow we form our menu.
The layout for the popup window is the table layout. And each item
of the menu we add to the layout. And to complete the menu we add
a listener to each item.
<br />
<pre class="java" name="code"> /**
* Opened menu
*/
private static volatile CustomMenu mMenu = null;
private ArrayList<CustomMenuItem> mMenuItems;
private OnMenuItemSelectedListener mListener = null;
private Activity mContext = null;
private static volatile PopupWindow mPopupWindow = null;
private static boolean mIsShowing = false;
/**
* Menu item selected listener
*/
public interface OnMenuItemSelectedListener {
public void MenuItemSelectedEvent(Integer selection);
}
/**
* Is the menu opened
*
* @return boolean isShowing
*/
public static boolean isShowing() {
return mIsShowing;
}
/**
* Constructor
*
* @param activity
* Activity where the menu will be shown
* @param OnMenuItemSelectedListener
* listener Listener
* @param LayoutInflater
* items Items list
* @return void
*/
public CustomMenu(Activity activity,
OnMenuItemSelectedListener listener,
HashMap<Integer, String> items) {
mListener = listener;
mMenuItems = new ArrayList<CustomMenuItem>();
mContext = activity;
// Add items to the menu
for (Integer id : items.keySet()) {
String name = items.get(id);
CustomMenuItem cmi = new CustomMenuItem();
cmi.setCaption(name);
cmi.setId(id);
mMenuItems.add(cmi);
}
}
public void show(View v) {
// The menu is shown
mIsShowing = true;
int itemCount = mMenuItems.size();
if (itemCount < 1)
return; // Nothing to show
if (mPopupWindow != null)
return; // The menu is opened
// Display settings
Display display = ((WindowManager) mContext
.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
// The view to show
View mView = ((Activity) mContext).getLayoutInflater().inflate(
R.layout.custom_menu, null);
// Create popup window to show
mPopupWindow = new PopupWindow(mView, LayoutParams.FILL_PARENT,
LayoutParams.WRAP_CONTENT, false);
mPopupWindow.setAnimationStyle(R.style.Animations_MenuAnimation);
mPopupWindow.setWidth(display.getWidth());
mPopupWindow.showAtLocation(v, Gravity.BOTTOM, 0, 0);
// Add menu items
TableLayout table = (TableLayout) mView
.findViewById(R.id.custom_menu_table);
table.removeAllViews();
for (int i = 0; i < itemCount; i++) {
TableRow row = null;
Button btn = null;
// create headers
row = new TableRow(mContext);
row.setLayoutParams(new LayoutParams(
LayoutParams.FILL_PARENT,
LayoutParams.WRAP_CONTENT));
final CustomMenuItem cmi = mMenuItems.get(i);
View itemLayout = ((Activity) mContext).getLayoutInflater()
.inflate(R.layout.custom_menu_item, null);
btn = (Button) itemLayout
.findViewById(R.id.custom_menu_item_caption);
btn.setText(cmi.getCaption());
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.MenuItemSelectedEvent(cmi.getId());
hide();
}
});
row.addView(itemLayout);
table.addView(row);
}
}
</pre>
<br />
And we have:
<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiA-IuCy6dJUD43vTRgcza5Jfp6QOfle_ABrTaqvqv9OyuxfdGe5VLAJd-sGU_0L8YvirSlSJ7ckL8J0sTj32kXWunJ5WZw3wwDvIHHJLBgAiULcIOau9AD0WmiL_18ojqx3F8E27qAQaka/s1600/acsheet.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiA-IuCy6dJUD43vTRgcza5Jfp6QOfle_ABrTaqvqv9OyuxfdGe5VLAJd-sGU_0L8YvirSlSJ7ckL8J0sTj32kXWunJ5WZw3wwDvIHHJLBgAiULcIOau9AD0WmiL_18ojqx3F8E27qAQaka/s320/acsheet.png" width="213" /></a></div>
<br />
<br /></div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com7tag:blogger.com,1999:blog-426859432260012411.post-8659730606309148882011-12-26T18:29:00.000+04:002011-12-26T18:37:02.907+04:00SAMSUNG Galaxy S ICS Petition!<div dir="ltr" style="text-align: left;" trbidi="on">
Some angry Samsung Galaxy S users have done petition to get Samsung to give the GS Android4.0 Ice Cream Sandwich, its already 8000+ signatures strong and growing:<br />
<br />
<a href="http://change.org/petitions/wwwsamsungcom-distribution-of-ice-cream-sandwich-version-for-users-of-the-samsung-i9000">http://change.org/petitions/wwwsamsungcom-distribution-of-ice-cream-sandwich-version-for-users-of-the-samsung-i9000</a><br />
<br />
Please sign it if you believe SAMSUNG made the wrong decision when leaving the Galaxy i900x devices out of their ICS update roadmap.</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-426859432260012411.post-27396389683529934832011-12-19T11:00:00.001+04:002011-12-27T12:45:38.577+04:00How to use the raw resource files in the native code<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
<a href="http://www.blogger.com/blogger.g?blogID=426859432260012411#"><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Russian translation. Pending...</div></a><br />
<br />
As you know the Android application is packed to the ".apk" container and all resources of the application are there.
But what if you want to use a resource file inside the native code?
Sure, you may move it to the external card and do with it all what you want.
But it seems to be a slightly clumsy way.
Really, I want to treat a raw resource file as it was an ordinary file stream.
What we have at start: java code able to open and work with raw data files.
Here is the sample code:
<br />
<pre class="java" name="code">
AssetFileDescriptor descriptor = context.getResources().
openRawResourceFd(R.raw.rawFile);
FileDescriptor fd = desc.getFileDescriptor();
</pre>
Let's look at the documentation:<br />
<br />
<blockquote class="tr_bq">
Instances of the file descriptor class serve as an opaque handle to the underlying machine-specific structure representing an open file, an open socket, or another source or sink of bytes.</blockquote>
So it is what we are looking for.
Moreover from the "AssetFileDescriptor" we can get the length of the file and its offset in the "apk" file.
<br />
<pre class="java" name="code"> long offset = descriptor.getStartOffset();
long lenth = descriptor.getLength();
</pre>
And, at last, how to get the real native file descriptor:
<br />
<pre class="java" name="code"> try {
Field field = fd.getClass().getDeclaredField("descriptor");
field.setAccessible(true);
nativeDescriptor = (Integer) field.get(fd);
} catch (NoSuchFieldException e1) {
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
}
</pre>
Again, let's look at the documentation about "raw" files:
<br />
<br />
<blockquote class="tr_bq">
Arbitrary files to save in their raw form</blockquote>
So, they are saved as is - without compression and we can read them.
Now we may go to the native part with following entry data:<br />
<br />
1. The native file descriptor of the "apk" file.<br />
2. The Offset of the "raw" file.<br />
3. The length of the "raw" file.<br />
<br />
The base idea is to map this part of the "apk" file into a memory and
use the "fmemopen" function to get the "FILE" structure.
Android doesn't support the "fmemopen" function, but you can find
its implementation here:
<br />
<br />
<a href="http://code.google.com/p/tesseract-android-tools/source/browse/trunk/jni/com_googlecode_leptonica_android/stdio/fmemopen.c?r=2"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">fmemopen.c</div></i></a>
<br />
And at last:
<br />
<pre class="c++" name="code"> int desc = dup(fd);
long pa_offset = off & ~(sysconf(_SC_PAGE_SIZE) - 1);
void *mapped = mmap(NULL, len + off - pa_offset, PROT_READ,
MAP_PRIVATE, desc, pa_offset);
if (mapped == MAP_FAILED) {
... error handling...
}
FILE *memoFile = fmemopen((void*) (mapped + off - pa_offset), len, "r");
if (memoFile == NULL){
... error handling...
}
</pre>
And don't forget to free resources...
</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com2tag:blogger.com,1999:blog-426859432260012411.post-71470581107714798622011-12-13T11:18:00.001+04:002011-12-27T12:49:30.276+04:00Source code<div dir="ltr" style="text-align: left;" trbidi="on">
The source code for all articles was moved to the Google Code.
There are links for articles:
3D Carousel Demo:<br/>
<a href="https://code.google.com/p/android-carousel-demo/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">3D Carousel</div></i></a>
<br/>
<br/>
ImageView with SVG support:<br/>
<a href="https://code.google.com/p/image-view-svg/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">SVG ImageView</div></i></a>
<br/>
<br/>
All other articles are combined into one project:<br/>
<a href="https://code.google.com/p/android-tips-demo/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Android.Tips</div></i></a>
<br/>
<br/>
Links in articles are updated too.</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-426859432260012411.post-21205793298762245372011-12-05T10:26:00.001+04:002011-12-27T12:51:41.475+04:00Using a mask with EditText<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
<br />
<a href="https://code.google.com/p/android-tips-demo/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Source Code</div></i></a><br />
<a href="http://horribileru.blogspot.com/2011/12/textedit.html"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Русский перевод.</div></i></a><br />
<br />
To continue the theme about formatting a text with regular expressions we will<br />
implement a functionality for using a mask with the EditText control.<br />
<br />
Sure we can use InputType, but it would be nice to have more flexible functionality.<br />
<br />
<br />
To begin let's look at the MaskFormatter class of the Swing framework.<br />
It has all what we need. So, let's make some changes in this class.<br />
We leave inner classes as is. But the MaskFormatter class works with<br />
the JFormattedTextField control. We must remove this appendix.<br />
As a result we have something like this:
<br />
<pre class="java" name="code">public class MaskedFormatter {
// Potential values in mask.
private static final char DIGIT_KEY = '#';
private static final char LITERAL_KEY = '\'';
private static final char UPPERCASE_KEY = 'U';
private static final char LOWERCASE_KEY = 'L';
private static final char ALPHA_NUMERIC_KEY = 'A';
private static final char CHARACTER_KEY = '?';
private static final char ANYTHING_KEY = '*';
private static final char HEX_KEY = 'H';
/** The user specified mask. */
private String mask;
/** Indicates if the value contains the literal characters. */
private boolean containsLiteralChars;
private static final MaskCharacter[] EmptyMaskChars =
new MaskCharacter[0];
/** List of valid characters. */
private String validCharacters;
/** List of invalid characters. */
private String invalidCharacters;
/** String used to represent characters not present. */
private char placeholder;
/** String used for the passed in value if it does not completely
* fill the mask. */
private String placeholderString;
private transient MaskCharacter[] maskChars;
/** Indicates if the value being edited must match the mask. */
@SuppressWarnings("unused")
private boolean allowsInvalid;
/**
* Creates a MaskFormatter with no mask.
*/
public MaskedFormatter() {
setAllowsInvalid(false);
containsLiteralChars = true;
maskChars = EmptyMaskChars;
placeholder = ' ';
}
/**
* Creates a MaskFormatter with the specified mask.
* A ParseException
* will be thrown if mask is an invalid mask.
*
* @throws ParseException if mask does not contain valid mask characters
*/
public MaskedFormatter(String mask) throws ParseException {
this();
setMask(mask);
}
/**
* Sets the mask dictating the legal characters.
* This will throw a ParseException if mask is
* not valid.
*
* @throws ParseException if mask does not contain valid mask characters
*/
public void setMask(String mask) throws ParseException {
this.mask = mask;
updateInternalMask();
}
/**
* Returns the formatting mask.
*
* @return Mask dictating legal character values.
*/
public String getMask() {
return mask;
}
/**
* Updates the internal representation of the mask.
*/
private void updateInternalMask() throws ParseException {
String mask = getMask();
ArrayList<MaskCharacter> fixed = new ArrayList<MaskCharacter>();
ArrayList<MaskCharacter> temp = fixed;
if (mask != null) {
for (int counter = 0, maxCounter = mask.length();
counter < maxCounter; counter++) {
char maskChar = mask.charAt(counter);
switch (maskChar) {
case DIGIT_KEY:
temp.add(new DigitMaskCharacter());
break;
case LITERAL_KEY:
if (++counter < maxCounter) {
maskChar = mask.charAt(counter);
temp.add(new LiteralCharacter(maskChar));
}
// else: Could actually throw if else
break;
case UPPERCASE_KEY:
temp.add(new UpperCaseCharacter());
break;
case LOWERCASE_KEY:
temp.add(new LowerCaseCharacter());
break;
case ALPHA_NUMERIC_KEY:
temp.add(new AlphaNumericCharacter());
break;
case CHARACTER_KEY:
temp.add(new CharCharacter());
break;
case ANYTHING_KEY:
temp.add(new MaskCharacter());
break;
case HEX_KEY:
temp.add(new HexCharacter());
break;
default:
temp.add(new LiteralCharacter(maskChar));
break;
}
}
}
if (fixed.size() == 0) {
maskChars = EmptyMaskChars;
}
else {
maskChars = new MaskCharacter[fixed.size()];
fixed.toArray(maskChars);
}
}
/**
* Sets whether or not the value being edited is allowed to be invalid
* for a length of time (that is, stringToValue throws
* a ParseException).
* It is often convenient to allow the user to temporarily input an
* invalid value.
*
* @param allowsInvalid Used to indicate if the edited value must always
* be valid
*/
public void setAllowsInvalid(boolean allowsInvalid) {
this.allowsInvalid = allowsInvalid;
}
/**
* Allows for further restricting of the characters that can be input.
* Only characters specified in the mask, not in the
* invalidCharacters, and in
* validCharacters will be allowed to be input. Passing
* in null (the default) implies the valid characters are only bound
* by the mask and the invalid characters.
*
* @param validCharacters If non-null, specifies legal characters.
*/
public void setValidCharacters(String validCharacters) {
this.validCharacters = validCharacters;
}
/**
* Returns the valid characters that can be input.
*
* @return Legal characters
*/
public String getValidCharacters() {
return validCharacters;
}
/**
* Allows for further restricting of the characters that can be input.
* Only characters specified in the mask, not in the
* invalidCharacters, and in
* validCharacters will be allowed to be input. Passing
* in null (the default) implies the valid characters are only bound
* by the mask and the valid characters.
*
* @param invalidCharacters If non-null, specifies illegal characters.
*/
public void setInvalidCharacters(String invalidCharacters) {
this.invalidCharacters = invalidCharacters;
}
/**
* Returns the characters that are not valid for input.
*
* @return illegal characters.
*/
public String getInvalidCharacters() {
return invalidCharacters;
}
/**
* If true, the returned value and set value will also contain the literal
* characters in mask.
*
* For example, if the mask is '(###) ###-####', the
* current value is '(415) 555-1212', and
* valueContainsLiteralCharacters is
* true stringToValue will return
* '(415) 555-1212'. On the other hand, if
* valueContainsLiteralCharacters is false,
* stringToValue will return '4155551212'.
*
* @param containsLiteralChars Used to indicate if literal characters in
* mask should be returned in stringToValue
*/
public void setValueContainsLiteralCharacters(
boolean containsLiteralChars) {
this.containsLiteralChars = containsLiteralChars;
}
/**
* Returns true if stringToValue should return literal
* characters in the mask.
*
* @return True if literal characters in mask should be returned in
* stringToValue
*/
public boolean getValueContainsLiteralCharacters() {
return containsLiteralChars;
}
/**
* Sets the character to use in place of characters that are not present
* in the value, ie the user must fill them in. The default value is
* a space.
*
* This is only applicable if the placeholder string has not been
* specified, or does not completely fill in the mask.
*
* @param placeholder Character used when formatting if the value does not
* completely fill the mask
*/
public void setPlaceholderCharacter(char placeholder) {
this.placeholder = placeholder;
}
/**
* Returns the character to use in place of characters that are not present
* in the value, ie the user must fill them in.
*
* @return Character used when formatting if the value does not
* completely fill the mask
*/
public char getPlaceholderCharacter() {
return placeholder;
}
/**
* Sets the string to use if the value does not completely fill in
* the mask. A null value implies the placeholder char should be used.
*
* @param placeholder String used when formatting if the value does not
* completely fill the mask
*/
public void setPlaceholder(String placeholder) {
this.placeholderString = placeholder;
}
/**
* Returns the String to use if the value does not completely fill
* in the mask.
*
* @return String used when formatting if the value does not
* completely fill the mask
*/
public String getPlaceholder() {
return placeholderString;
}
/**
* Returns a String representation of the Object value
* based on the mask. Refer to
* {@link #setValueContainsLiteralCharacters} for details
* on how literals are treated.
*
* @throws ParseException if there is an error in the conversion
* @param value Value to convert
* @see #setValueContainsLiteralCharacters
* @return String representation of value
*/
public String valueToString(Object value) throws ParseException {
String sValue = (value == null) ? "" : value.toString();
StringBuilder result = new StringBuilder();
String placeholder = getPlaceholder();
int[] valueCounter = { 0 };
append(result, sValue, valueCounter, placeholder, maskChars);
return result.toString();
}
/**
* Invokes append on the mask characters in
* mask.
*/
private void append(StringBuilder result, String value, int[] index,
String placeholder, MaskCharacter[] mask)
throws ParseException {
for (int counter = 0, maxCounter = mask.length;
counter < maxCounter; counter++) {
mask[counter].append(result, value, index, placeholder);
}
}
</pre>
<br />
And to simplify the life we will create a TextWatcher class to work with the formatter:<br />
<pre class="java" name="code"> public class MaskedWatcher implements TextWatcher {
private String mMask;
String mResult = "";
public MaskedWatcher(String mask){
mMask = mask;
}
@Override
public void afterTextChanged(Editable s) {
String mask = mMask;
String value = s.toString();
if(value.equals(mResult))
return;
try {
// prepare the formatter
MaskedFormatter formatter = new MaskedFormatter(mask);
formatter.setValueContainsLiteralCharacters(false);
formatter.setPlaceholderCharacter((char)1);
// get a string with applied mask and placeholder chars
value = formatter.valueToString(value);
try{
// find first placeholder
value = value.substring(0, value.indexOf((char)1));
//process a mask char
if(value.charAt(value.length()-1) ==
mask.charAt(value.length()-1)){
value = value.substring(0, value.length() - 1);
}
}
catch(Exception e){}
mResult = value;
s.replace(0, s.length(), value);
} catch (ParseException e) {
//the entered value does not match a mask
int offset = e.getErrorOffset();
value = removeCharAt(value, offset);
s.replace(0, s.length(), value);
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
}
public static String removeCharAt(String s, int pos) {
StringBuffer buffer = new StringBuffer(s.length() - 1);
buffer.append(s.substring(0, pos)).append(s.substring(pos + 1));
return buffer.toString();
}
}
</pre>
<br />
And now we can work with masks:<br />
<pre class="java" name="code"> EditText phone = (EditText)findViewById(R.id.phone);
phone.addTextChangedListener(
new MaskedWatcher("(###) ###-##-##")
)
</pre>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGBkJ8Qzjx3I6hu0__2-bwbyoReqVo9POjxAUDBnikaoP8nybnEfPJMQhPD0UarBcML6mVJEUwf7Drc_tIEuGSAI5Ky2IdWJWSDt_r57-q3qmmR4GC6xdh_OptaqGmtIe7iIemYpMVXXRu/s1600/msk.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjGBkJ8Qzjx3I6hu0__2-bwbyoReqVo9POjxAUDBnikaoP8nybnEfPJMQhPD0UarBcML6mVJEUwf7Drc_tIEuGSAI5Ky2IdWJWSDt_r57-q3qmmR4GC6xdh_OptaqGmtIe7iIemYpMVXXRu/s1600/msk.png" /></a></div>
<br />
<br />
<br /></div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com34tag:blogger.com,1999:blog-426859432260012411.post-36042426097037542582011-11-30T16:24:00.001+04:002011-11-30T16:29:00.805+04:00Android 3D Carousel bug. Android 3.0+In the "CarouselItem" class there is the "getMatrix" method. But since API level 11 there is such method in View class. Just rename this method to something else in CarouselItem class and in the "pointToPosition" method of the "CarouselSpinner" class. <br />
<br />
In the near future I will update the source code.<br />
<br />
ThanksHorribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-426859432260012411.post-28792263223757783822011-11-30T12:57:00.000+04:002011-11-30T12:57:06.171+04:00ARM released Development Studio CEARM has released a new Development Studio Community Edition. The tools work alongside the existing Android Native Development Kit (NDK), and allow low-level access to the processor in ARM devices — crucial to creating high-performance applications on any platform. Ultimately, this means that apps can be written in native code (C or C++)...<br />
<br />
It's cool. It's worth a try...Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-426859432260012411.post-74582460715659645722011-11-18T13:51:00.002+04:002011-12-27T12:53:43.589+04:00Formatting EditText input with regular expressions.<div dir="ltr" style="text-align: left;" trbidi="on">
<a href="https://code.google.com/p/android-tips-demo/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Source Code</div></i></a><br />
<a href="http://horribileru.blogspot.com/2011/12/edittext.html"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Русский перевод.</div></i></a><br />
<br />
It will be the short article. Now we will format text in EditText with regular expressions.<br />
<br />
The simple class will extend Input Filter:<br />
<br />
<pre class="java" name="code">public class PartialRegexInputFilter implements InputFilter {
private Pattern mPattern;
public PartialRegexInputFilter(String pattern){
mPattern = Pattern.compile(pattern);
}
@Override
public CharSequence filter(CharSequence source,
int sourceStart, int sourceEnd,
Spanned destination, int destinationStart,
int destinationEnd)
{
String textToCheck = destination.subSequence(0, destinationStart).
toString() + source.subSequence(sourceStart, sourceEnd) +
destination.subSequence(
destinationEnd, destination.length()).toString();
Matcher matcher = mPattern.matcher(textToCheck);
// Entered text does not match the pattern
if(!matcher.matches()){
// It does not match partially too
if(!matcher.hitEnd()){
return "";
}
}
return null;
}
}
</pre>
<br />
The trick is that if the input text does not match the pattern it can match it partially.<br />
If so we will allow the text pasting.<br />
<br />
And finally formatting a phone number:<br />
<br />
<pre class="java" name="code">final String regex = "\\(\\d{3}\\)\\d{3}\\-\\d{2}\\-\\d{2}";
txt.setFilters(
new InputFilter[] {
new PartialRegexInputFilter(regex)
}
);
txt.addTextChangedListener(
new TextWatcher(){
@Override
public void afterTextChanged(Editable s) {
String value = s.toString();
if(value.matches(regex))
txt.setTextColor(Color.BLACK);
else
txt.setTextColor(Color.RED);
}
@Override
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {}
}
);
</pre>
<br />
<br />
We have: <br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiftSTX6uf1ubjZjXg7snk407_UesXPd7sVfdfBfqzBBcu3OtI94fRyXhiODyGBM7JdGGAdg0UM3MH7PzQv3zebvJyNr5L7r1_G7hyphenhyphenKIKzYIum2Rp5raf71nyBkm6EFPUQifIKP6CBjhIuP/s1600/regformatting.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiftSTX6uf1ubjZjXg7snk407_UesXPd7sVfdfBfqzBBcu3OtI94fRyXhiODyGBM7JdGGAdg0UM3MH7PzQv3zebvJyNr5L7r1_G7hyphenhyphenKIKzYIum2Rp5raf71nyBkm6EFPUQifIKP6CBjhIuP/s320/regformatting.png" width="192" /></a></div>
<br />
<br />
<br /></div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com6tag:blogger.com,1999:blog-426859432260012411.post-44314859461292261082011-11-08T14:43:00.001+04:002011-12-27T12:55:16.029+04:00Two activities on the screen at the same time<div dir="ltr" style="text-align: left;" trbidi="on"><a href="https://code.google.com/p/android-tips-demo/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Source code</div></i></a><br />
<a href="http://horribileru.blogspot.com/2011/11/blog-post.html"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Русский перевод.</div></i></a><br />
<br />
Perhaps you've seen that some applications such as winamp, gimp and so on have several separated windows. It was interesting if it's possible to implement such a functionality on Android. Sure it could be done just with layouts, but we are not looking for easy ways.<br />
<br />
First, let's define styles:<br />
<br />
<pre class="xml" name="code"><style name="Theme.Transparent" parent="android:Theme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
<style name="Theme.Transparent.Floating">
<item name="android:windowIsFloating">true</item>
</style>
</pre><br />
The "<span class="Apple-style-span" style="color: red;">Theme.Transparent</span>" style is like one we used for the splash screen but lacks "<span class="Apple-style-span" style="color: red;">windowIsFloating</span>" item. Due to this the first activity will fill the screen. And for the second activity there is "<span class="Apple-style-span" style="color: red;">Theme.Transparent.Floating</span>" style. So this activity won't fill the screen and our touches will be available for the first onу. No. By default activities are modal. And touches won't be available. We must do such workaround:<br />
<br />
<pre class="java" name="code">getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
</pre><br />
Ok, now we see the screen of the device, two activities and first activity is available behind the scene.<br />
But there is one thing to do: communication between these activities. In normal workflow we use<br />
startActivityForResult. But it's not applicable in this case. The simplest way to make them know one of another - to broadcast custom action:<br />
<br />
<pre class="java" name="code">mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// The first activity wants to close this one
String operation = intent.getStringExtra("operation");
if(operation.equals("hide"))
finish();
}
};
</pre><br />
<br />
And as a result:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgV3h347gSbreUov2OQaO7oK1zYC8FCm1_uSlAmNM3_eQaA-G3lcZ0gXub1m-_9p7m-47QG_akBi5UYfKvkkFEHJ3CwuikeQ36eRziqkEo_BEvzBjrwntacrEwl8QLuO35v1bH9zlAeMasQ/s1600/taos.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgV3h347gSbreUov2OQaO7oK1zYC8FCm1_uSlAmNM3_eQaA-G3lcZ0gXub1m-_9p7m-47QG_akBi5UYfKvkkFEHJ3CwuikeQ36eRziqkEo_BEvzBjrwntacrEwl8QLuO35v1bH9zlAeMasQ/s320/taos.png" width="192" /></a></div><br />
<br />
<br />
Maybe it will be usefull for something.<br />
<br />
<br />
</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-426859432260012411.post-59191805952783082062011-11-03T08:43:00.000+04:002011-12-27T12:56:27.339+04:00Android. iPhone-style dialog. XML – only.<div dir="ltr" style="text-align: left;" trbidi="on"><a href="https://code.google.com/p/android-tips-demo/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Source Code</div></i></a><br />
<a href="http://horribileru.blogspot.com/2011/11/error-iphone-xml.html"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Русский перевод</div></i></a><br />
<br />
As the next exercise I will show how to implement the dialog that looks like iPhone alert view. No pictures will be used. We will use only XML.<br />
<br />
Really it’s simple. First, let’s define xml-drawable for the button:<br />
<div><br />
</div><br />
<pre class="xml" name="code"><layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape android:shape="rectangle" >
<corners android:radius="8dip" />
<gradient
android:angle="270"
android:endColor="#FF440000"
android:startColor="#FF990000"
android:type="linear" />
</shape></item>
<item android:top="20dip">
<shape android:shape="rectangle" >
<corners
android:bottomLeftRadius="8dp"
android:bottomRightRadius="8dp" />
<solid android:color="#40000000" />
</shape></item>
</layer-list>
</pre><br />
<br />
Here we have two layers. The first layer – the rectangle with the gradient. The second layer – the rectangle shifted 20 dip top. This layer should overlap the half of the first one. So, real button should be 40 dip high.<br />
<br />
Second, we’ll define the content of the dialog – the header, the text view for a message and the OK button:<br />
<br />
<pre class="xml" name="code"><?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical"
android:orientation="vertical" >
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/alert_wrapper"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dip"
android:layout_marginRight="20dip"
android:gravity="center_horizontal"
android:orientation="vertical" >
<TextView
android:id="@+id/dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dip"
android:text="Header container"
android:textColor="#ffffff"
android:textSize="17dip"
android:textStyle="bold" />
<TextView
android:id="@+id/dialog_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dip"
android:gravity="center_horizontal"
android:maxLines="5"
android:scrollbars="vertical"
android:text="Text container"
android:textColor="#ffffff"
android:textSize="15dip" />
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dip"
android:layout_marginTop="10dip"
android:gravity="center_horizontal"
android:orientation="horizontal" >
<Button
android:id="@+id/ok"
android:layout_width="fill_parent"
android:layout_height="40dip"
android:layout_marginBottom="10dip"
android:layout_marginLeft="10dip"
android:layout_marginRight="10dip"
android:background="@drawable/iphone_style_button"
android:text="@string/ok"
android:textColor="@color/White"
android:textSize="17dip"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</pre><br />
And at last the background for the dialog will be defined in a code. We will need a drawable with three layers for this – a rectangle shape with white border, a rectangle shape with main color and a rectangle shape that will contain a gloss effect.<br />
To make the white border visible under the first layer we’ll set insets for this layer:<br />
<br />
<br />
<br />
<pre class="java" name="code">// Layers array
Drawable[] arr = new Drawable[3];
float roundedCorner[] = new float[] { 8, 8, 8, 8, 8, 8, 8, 8 };
// First layer - to make a border
GradientDrawable first = new GradientDrawable();
first.setShape(GradientDrawable.RECTANGLE);
first.setCornerRadii(roundedCorner);
first.setStroke(2, Color.WHITE);
// Second layer - background
GradientDrawable second = new GradientDrawable();
second.setShape(GradientDrawable.RECTANGLE);
second.setCornerRadii(roundedCorner);
second.setColor(Color.argb(255, 127, 0, 0));
// Third layer - for the gloss effect
GlossDrawable third = new GlossDrawable();
arr[0] = first;
arr[1] = second;
arr[2] = third;
LayerDrawable background = new LayerDrawable(arr);
</pre><br />
<br />
<br />
The more complicated stuff is in the GlossDrawable class. There we will override onDraw method to calculate where the gloss gradient will be.<br />
<br />
The picture describing calculations:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2GysFMEPobJMDUU64YVSbj0hWiBIkL6wQsnPZtoPKtwoZlXPfPh02I7o5Je22RhG2LntC5-P7i2ga0jYQK5EnzWa2X2x_RLJpWgVjOiUds6b1mKXcxaMN4CXbRjPVqVxDQtUpZLz7s6__/s1600/ris.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="222" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2GysFMEPobJMDUU64YVSbj0hWiBIkL6wQsnPZtoPKtwoZlXPfPh02I7o5Je22RhG2LntC5-P7i2ga0jYQK5EnzWa2X2x_RLJpWgVjOiUds6b1mKXcxaMN4CXbRjPVqVxDQtUpZLz7s6__/s320/ris.PNG" width="320" /></a></div><br />
<br />
Using the Pythagoras' theorem we have sides of the inscribed triangle:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEigCwNMjgew7uzW74VUmTbQ-k-vlVP9bu-3ME_4bK3Z-7WgS4UrCxdOt7PybhxJ6IrKfb6MLrRfZk1oPubhfmO5kZ9wDlL0gJ5zswAhURmt_GG4GN5eemmnIh8vvFNN0Xg1GXtVy-V0W-rW/s1600/sides.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEigCwNMjgew7uzW74VUmTbQ-k-vlVP9bu-3ME_4bK3Z-7WgS4UrCxdOt7PybhxJ6IrKfb6MLrRfZk1oPubhfmO5kZ9wDlL0gJ5zswAhURmt_GG4GN5eemmnIh8vvFNN0Xg1GXtVy-V0W-rW/s1600/sides.PNG" /></a></div>Then using Heron’s formula we’ll find the area of the inscribed triangle:<br />
<span lang="EN-US" style="font-family: 'Times New Roman'; font-size: 12pt;"><span style="position: relative; top: 15pt;"></span></span><br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5M3dn7rIF21Zd36D4ZHBvN6hJVUfINTCc7GxhpWO8EepWCrCK2_45WRml9r08taH1tlXf3aVkE4nvQoP9EhcrcFi66-7bsP4O8aFeD8TqRY6nLCnZlbyPgqds71vHIlzctSkBPWrn1uuD/s1600/area.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5M3dn7rIF21Zd36D4ZHBvN6hJVUfINTCc7GxhpWO8EepWCrCK2_45WRml9r08taH1tlXf3aVkE4nvQoP9EhcrcFi66-7bsP4O8aFeD8TqRY6nLCnZlbyPgqds71vHIlzctSkBPWrn1uuD/s1600/area.PNG" /></a></div><br />
And at last the radius:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3RIhyphenhyphen4R9EzbAGKWM6lE3Ih5AyQvsDaebwXZU4ZQKlxKAdZ4RvcPaJdzqO5KlHZSGiZCV3C9gAY9x8MaXPQw7bXRwgIz4FvBblE9qxRytRMKf9D8U7VdMcPRnApvMMyMmJYWKXfLO42-68/s1600/rad.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3RIhyphenhyphen4R9EzbAGKWM6lE3Ih5AyQvsDaebwXZU4ZQKlxKAdZ4RvcPaJdzqO5KlHZSGiZCV3C9gAY9x8MaXPQw7bXRwgIz4FvBblE9qxRytRMKf9D8U7VdMcPRnApvMMyMmJYWKXfLO42-68/s1600/rad.PNG" /></a></div><br />
Now we should draw the circle slightly lower (1/8 of the shape height). The center of the circle will be:<br />
<br />
<br />
<pre class="java" name="code">int centerX = (int) shape.getWidth() / 2;
int centerY = (int) (-radius + shape.getHeight() / 2);
</pre><br />
The rectangle to draw the circle will be:<br />
<br />
<pre class="java" name="code">RectF rectf = new RectF(shape.getWidth() / 2 - radius,
shape.getHeight() / 4 - radius * 2,
shape.getWidth() / 2 + radius, shape.getHeight() / 4);
</pre><br />
Applying the gradient we have:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuwCAsrz09cySDXIJQuayBmwSWMhsZDCZkY1CCODfCzao3wY5wV8Qgr_vJH7WarKJwhRQ8BmmXiExJpa1tp2HSKuIIqbLA8ZGAc529a251JltENlMr9WLimrZlr6tZ0hBI55TwE032M9vz/s1600/dlg.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhuwCAsrz09cySDXIJQuayBmwSWMhsZDCZkY1CCODfCzao3wY5wV8Qgr_vJH7WarKJwhRQ8BmmXiExJpa1tp2HSKuIIqbLA8ZGAc529a251JltENlMr9WLimrZlr6tZ0hBI55TwE032M9vz/s1600/dlg.png" /></a></div><br />
<br />
Use the same technique to make info and confirm dialogs. All you need is to change the background color and the content layout.<br />
<br />
Cheerio!<br />
<br />
</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com2tag:blogger.com,1999:blog-426859432260012411.post-65262869630475930382011-11-01T13:39:00.001+04:002011-12-27T13:11:35.385+04:00Android 3D Carousel<div dir="ltr" style="text-align: left;" trbidi="on"><br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2dYtVlVHPJWeL53ONnDIa4SzK1H1iT94t9mKb0S_wGIiGvhtKTSzem5HfDKwPyn5cUY7MOfs-KCHvajQZatOv9-G-cAewxCMBIsdIwX4LyCUI5akj74A12HKWlZMpH9wPmF7EuGQAOeAa/s1600/carousel.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2dYtVlVHPJWeL53ONnDIa4SzK1H1iT94t9mKb0S_wGIiGvhtKTSzem5HfDKwPyn5cUY7MOfs-KCHvajQZatOv9-G-cAewxCMBIsdIwX4LyCUI5akj74A12HKWlZMpH9wPmF7EuGQAOeAa/s1600/carousel.png" /></a></div><br />
<div class="separator" style="clear: both; text-align: center;"></div><a href="https://code.google.com/p/android-carousel-demo/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Source Code</div></i></a><br />
<a href="http://horribileru.blogspot.com/2011/11/android-3d.html"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Русский перевод.</div></i></a><br />
<br />
<span class="Apple-style-span" style="background-color: #141414; color: white; font-family: Arial, Tahoma, Helvetica, FreeSans, sans-serif; font-size: 13px; line-height: 18px;">This my article was originally published on The Code Project web site. You can see it at <a href="http://www.codeproject.com/KB/android/androcarousel.aspx" style="color: #888888; text-decoration: none;"><div style="width:100%; font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">Codeproject.</div></a></span><br />
<h2>Introduction</h2>For a while, I was looking for a 3D carousel control for Android platform. The only one I found was <strong>UltimateFaves</strong> at [<a href="http://www.blogger.com/post-edit.g?blogID=426859432260012411&postID=6526286963047593038#ultimatefaves" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">1</a>]. But as it turned out, it uses OpenGL. And it’s not open source. I thought if it is possible to avoid a use of OpenGL. Continuing my investigations, I stamped on <strong>Coverflow Widget</strong> at [<a href="http://www.blogger.com/post-edit.g?blogID=426859432260012411&postID=6526286963047593038#coverflow" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">2</a>]. And it uses standard Android 2D libraries. So the idea was the same – to use Gallery class for the carousel. The Coverflow Widget just rotates images and I wanted to rotate all group of them. Well, at least it implies the use of simple trig methods. More complicated stuff goes with the Gallery class. If you’d look through the article about Coverflow Widget at [<a href="http://www.blogger.com/post-edit.g?blogID=426859432260012411&postID=6526286963047593038#coverflow1" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">3</a>], you’d see a bunch of problems, such as unavailability of default scope variables in <code>AbsSpinner </code>and <code>AdapterView </code>classes. So I went the same way and rewrote some classes. And the <code>Scroller </code>class will be replaced by the <code>Rotator </code>class which looks like <code>Scroller </code>but it rotates the group of images.<br />
<br />
<h2>The Preparations</h2>At first, we should decide what parameters will define a behavior of our Carousel. For example, a min quantity of items in the carousel. It will not look nice if it has only one or two items, won’t it? As for performance issue, we have to define max quantity of items. Also, we will need max theta angle for the carousel, what items will be in there, current selected item and if items will be reflected. So let’s define them in <em>attrs.xml</em> file:<br />
<br />
<pre class="xml" name="code"><?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Carousel">
<attr name="android:gravity" />
<attr name="android:animationDuration" />
<attr name="UseReflection" format="boolean"/>
<attr name="Items" format="integer"/>
<attr name="SelectedItem" format="integer"/>
<attr name="maxTheta" format="float"/>
<attr name="minQuantity" format="integer"/>
<attr name="maxQuantity" format="integer"/>
</declare-styleable>
</resources>
</pre><br />
<h2>The Carousel Item Class</h2>To simplify some stuff with carousel, I’ve created <code>CarouselItem</code>:<br />
<br />
<pre class="java" name="code">public class CarouselItem extends FrameLayout
implements Comparable<CarouselItem> {
private ImageView mImage;
private TextView mText;
private int index;
private float currentAngle;
private float x;
private float y;
private float z;
private boolean drawn;
// It's needed to find screen coordinates
private Matrix mMatrix;
public CarouselItem(Context context) {
super(context);
FrameLayout.LayoutParams params =
new FrameLayout.LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
this.setLayoutParams(params);
LayoutInflater inflater = LayoutInflater.from(context);
View itemTemplate = inflater.inflate(R.layout.item, this, true);
mImage = (ImageView)itemTemplate.findViewById(R.id.item_image);
mText = (TextView)itemTemplate.findViewById(R.id.item_text);
}
public String getName(){
return mText.getText().toString();
}
public void setIndex(int index) {
this.index = index;
}
public int getIndex() {
return index;
}
public void setCurrentAngle(float currentAngle) {
if(index == 0 && currentAngle > 5){
Log.d("", "");
}
this.currentAngle = currentAngle;
}
public float getCurrentAngle() {
return currentAngle;
}
public int compareTo(CarouselItem another) {
return (int)(another.z - this.z);
}
…
}
</pre><br />
It incapsulates the position in 3D space, the index of an item and the current angle of an item. Also implementing it as <code>Comparable </code>will be helpful when we’ll determine a draw order of the items. <br />
<h2>The Rotator Class </h2>If you’d look at the source code of <code>Scroller </code>class, you’ll see two modes: the scroll mode and the fling mode supposed just to calculate current offset from the given start point. We’ll just need to remove extra members, add our own and replace the corresponding calculations:<br />
<br />
<pre class="java" name="code">public class Rotator {
private int mMode;
private float mStartAngle;
private float mCurrAngle;
private long mStartTime;
private long mDuration;
private float mDeltaAngle;
private boolean mFinished;
private float mCoeffVelocity = 0.05f;
private float mVelocity;
private static final int DEFAULT_DURATION = 250;
private static final int SCROLL_MODE = 0;
private static final int FLING_MODE = 1;
private final float mDeceleration = 240.0f;
/**
* Create a Scroller with the specified interpolator.
* If the interpolator is null, the default (viscous)
* interpolator will be used.
*/
public Rotator(Context context) {
mFinished = true;
}
/**
*
* Returns whether the scroller has finished scrolling.
*
* @return True if the scroller has finished scrolling,
* false otherwise.
*/
public final boolean isFinished() {
return mFinished;
}
/**
* Force the finished field to a particular value.
*
* @param finished The new finished value.
*/
public final void forceFinished(boolean finished) {
mFinished = finished;
}
/**
* Returns how long the scroll event will take, in milliseconds.
*
* @return The duration of the scroll in milliseconds.
*/
public final long getDuration() {
return mDuration;
}
/**
* Returns the current X offset in the scroll.
*
* @return The new X offset as an absolute distance from the origin.
*/
public final float getCurrAngle() {
return mCurrAngle;
}
/**
* @hide
* Returns the current velocity.
*
* @return The original velocity less the deceleration.
* Result may be negative.
*/
public float getCurrVelocity() {
return mCoeffVelocity * mVelocity - mDeceleration
* timePassed() /* / 2000.0f*/;
}
/**
* Returns the start X offset in the scroll.
*
* @return The start X offset as an absolute distance from the origin.
*/
public final float getStartAngle() {
return mStartAngle;
}
/**
* Returns the time elapsed since the beginning of the scrolling.
*
* @return The elapsed time in milliseconds.
*/
public int timePassed() {
return (int)(AnimationUtils.currentAnimationTimeMillis() -
mStartTime);
}
/**
* Extend the scroll animation. This allows
* a running animation to scroll further and longer,
* when used with {@link #setFinalX(int)}
* or {@link #setFinalY(int)}.
*
* @param extend Additional time to scroll in milliseconds.
* @see #setFinalX(int)
* @see #setFinalY(int)
*/
public void extendDuration(int extend) {
int passed = timePassed();
mDuration = passed + extend;
mFinished = false;
}
/**
* Stops the animation. Contrary to {@link #forceFinished(boolean)},
* aborting the animating cause the scroller to
* move to the final x and y position
*
* @see #forceFinished(boolean)
*/
public void abortAnimation() {
mFinished = true;
}
/**
* Call this when you want to know the new location.
* If it returns true, the animation is not yet finished.
* loc will be altered to provide the
* new location.
*/
public boolean computeAngleOffset()
{
if (mFinished) {
return false;
}
long systemClock = AnimationUtils.currentAnimationTimeMillis();
long timePassed = systemClock - mStartTime;
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float sc = (float)timePassed / mDuration;
mCurrAngle = mStartAngle +
Math.round(mDeltaAngle * sc);
break;
case FLING_MODE:
float timePassedSeconds = timePassed / 1000.0f;
float distance;
if(mVelocity < 0)
{
distance = mCoeffVelocity *
mVelocity * timePassedSeconds -
(mDeceleration * timePassedSeconds *
timePassedSeconds / 2.0f);
}
else{
distance = -mCoeffVelocity * mVelocity *
timePassedSeconds - (mDeceleration *
timePassedSeconds * timePassedSeconds / 2.0f);
}
mCurrAngle = mStartAngle - Math.signum(mVelocity)*
Math.round(distance);
break;
}
return true;
}
else
{
mFinished = true;
return false;
}
}
/**
* Start scrolling by providing a starting point
* and the distance to travel.
*
* @param startX Starting horizontal scroll
* offset in pixels. Positive numbers will
* scroll the content to the left.
* @param startY Starting vertical scroll
* offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel.
* Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel.
* Positive numbers will scroll the content up.
* @param duration Duration of the scroll
* in milliseconds.
*/
public void startRotate(float startAngle, float dAngle, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartAngle = startAngle;
mDeltaAngle = dAngle;
}
/**
* Start scrolling by providing a starting point and the
* distance to travel. The scroll will use the default
* value of 250 milliseconds for the duration.
*
* @param startX Starting horizontal scroll
* offset in pixels. Positive numbers will
* scroll the content to the left.
* @param startY Starting vertical scroll
* offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel.
* Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel.
* Positive numbers will scroll the content up.
*/
public void startRotate(float startAngle, float dAngle) {
startRotate(startAngle, dAngle, DEFAULT_DURATION);
}
/**
* Start scrolling based on a fling gesture.
* The distance travelled will
* depend on the initial velocity of the fling.
*
* @param velocityAngle Initial velocity of the fling (X)
* measured in pixels per second.
*/
public void fling(float velocityAngle) {
mMode = FLING_MODE;
mFinished = false;
float velocity = velocityAngle;
mVelocity = velocity;
mDuration = (int)(1000.0f * Math.sqrt(2.0f * mCoeffVelocity *
Math.abs(velocity)/mDeceleration));
mStartTime = AnimationUtils.currentAnimationTimeMillis();
}
}
</pre><br />
<h2>The CarouselSpinner Differences with the AbsSpinner </h2>First, it extends <code>CarouselAdapter </code>vs <code>AdapterView</code>. Those differences I’ll describe later. Second, the modified constructor where the retrieving of <code>AbsSpinner </code>entries were removed. The third difference is modified <code>setSelection(int)</code> method. It was just call to <code>setSelectionInt </code>left. The next change is unavailable variables were replaced with their getters. As for default generated layout parameters, both were set to <code>WRAP_CONTENT</code>. The main changes concern <code>pointToPosition </code>method. In <code>AbsSpinner</code>, it determines if definite item was touched on a screen no matter whether it’s current or not. So, we beed to make the projection from 3D space to screen coordinates:<br />
<br />
<pre class="java" name="code">public int pointToPosition(int x, int y) {
ArrayList<CarouselItem> fitting = new ArrayList<CarouselItem>();
for(int i = 0; i < mAdapter.getCount(); i++){
CarouselItem item = (CarouselItem)getChildAt(i);
Matrix mm = item.getMatrix();
float[] pts = new float[3];
pts[0] = item.getLeft();
pts[1] = item.getTop();
pts[2] = 0;
mm.mapPoints(pts);
int mappedLeft = (int)pts[0];
int mappedTop = (int)pts[1];
pts[0] = item.getRight();
pts[1] = item.getBottom();
pts[2] = 0;
mm.mapPoints(pts);
int mappedRight = (int)pts[0];
int mappedBottom = (int)pts[1];
if(mappedLeft < x && mappedRight > x &
mappedTop < y && mappedBottom > y)
fitting.add(item);
}
Collections.sort(fitting);
if(fitting.size() != 0)
return fitting.get(0).getIndex();
else
return mSelectedPosition;
}
</pre><br />
<h2>The CarouselAdapter vs. AdapterView </h2>The only changes are in <code>updateEmptyStatus </code>method where unavailable variables were replaced with their getters. <br />
<h2>The Carousel Class</h2>Here <code>FlingRunnable </code>class was replaced with <code>FlingRotateRunnable </code>which is much like <code>FlingRunnable </code>but makes deal with angle vs. x-coordinate:<br />
<br />
<pre class="java" name="code">private class FlingRotateRunnable implements Runnable {
/**
* Tracks the decay of a fling rotation
*/
private Rotator mRotator;
/**
* Angle value reported by mRotator on the previous fling
*/
private float mLastFlingAngle;
/**
* Constructor
*/
public FlingRotateRunnable(){
mRotator = new Rotator(getContext());
}
private void startCommon() {
// Remove any pending flings
removeCallbacks(this);
}
public void startUsingVelocity(float initialVelocity) {
if (initialVelocity == 0) return;
startCommon();
mLastFlingAngle = 0.0f;
mRotator.fling(initialVelocity);
post(this);
}
public void startUsingDistance(float deltaAngle) {
if (deltaAngle == 0) return;
startCommon();
mLastFlingAngle = 0;
synchronized(this)
{
mRotator.startRotate(0.0f, -deltaAngle, mAnimationDuration);
}
post(this);
}
public void stop(boolean scrollIntoSlots) {
removeCallbacks(this);
endFling(scrollIntoSlots);
}
private void endFling(boolean scrollIntoSlots) {
/*
* Force the scroller's status to finished
(without setting its position to the end)
*/
synchronized(this){
mRotator.forceFinished(true);
}
if (scrollIntoSlots) scrollIntoSlots();
}
public void run() {
if (Carousel.this.getChildCount() == 0) {
endFling(true);
return;
}
mShouldStopFling = false;
final Rotator rotator;
final float angle;
boolean more;
synchronized(this){
rotator = mRotator;
more = rotator.computeAngleOffset();
angle = rotator.getCurrAngle();
}
// Flip sign to convert finger direction to
// list items direction (e.g. finger moving down
// means list is moving towards the top)
float delta = mLastFlingAngle - angle;
//////// Should be reworked
trackMotionScroll(delta);
if (more && !mShouldStopFling) {
mLastFlingAngle = angle;
post(this);
} else {
mLastFlingAngle = 0.0f;
endFling(true);
}
}
}
</pre>I also added <code>ImageAdapter </code>class as it is in Coverflow Widget with a possibility to add a reflection to the images. And some new <code>private </code>variables were added to support Y-axe angle, reflection and so on. The constructor retrieves list of images, creates <code>ImageAdapter </code>and sets it. The main thing in the constructor is setting the object to support <code>static </code>transformations. And to place images into their places:<br />
<br />
<pre class="java" name="code">/**
* Setting up images
*/
void layout(int delta, boolean animate){
if (mDataChanged) {
handleDataChanged();
}
// Handle an empty gallery by removing all views.
if (this.getCount() == 0) {
resetList();
return;
}
// Update to the new selected position.
if (mNextSelectedPosition >= 0) {
setSelectedPositionInt(mNextSelectedPosition);
}
// All views go in recycler while we are in layout
recycleAllViews();
// Clear out old views
detachAllViewsFromParent();
int count = getAdapter().getCount();
float angleUnit = 360.0f / count;
float angleOffset = mSelectedPosition * angleUnit;
for(int i = 0; i< getAdapter().getCount(); i++){
float angle = angleUnit * i - angleOffset;
if(angle < 0.0f)
angle = 360.0f + angle;
makeAndAddView(i, angle);
}
// Flush any cached views that did not get reused above
mRecycler.clear();
invalidate();
setNextSelectedPositionInt(mSelectedPosition);
checkSelectionChanged();
////////mDataChanged = false;
mNeedSync = false;
updateSelectedItemMetadata();
}
</pre><br />
Here are the methods to set up images. The height of an image is set <br />
three times lesser than parent height to make the carousel fit parent <br />
view. It should be reworked later.<br />
<br />
<pre class="java" name="code">private void makeAndAddView(int position, float angleOffset) {
CarouselItem child;
if (!mDataChanged) {
child = (CarouselItem)mRecycler.get(position);
if (child != null) {
// Position the view
setUpChild(child, child.getIndex(), angleOffset);
}
else
{
// Nothing found in the recycler --
// ask the adapter for a view
child = (CarouselItem)mAdapter.
getView(position, null, this);
// Position the view
setUpChild(child,
child.getIndex(), angleOffset);
}
return;
}
// Nothing found in the recycler --
// ask the adapter for a view
child = (CarouselItem)mAdapter.
getView(position, null, this);
// Position the view
setUpChild(child,
child.getIndex(), angleOffset);
}
private void setUpChild(CarouselItem child,
int index, float angleOffset) {
// Ignore any layout parameters for child,
// use wrap content
addViewInLayout(child, -1 /*index*/,
generateDefaultLayoutParams());
child.setSelected(index == mSelectedPosition);
int h;
int w;
int d;
if(mInLayout)
{
w = child.getMeasuredWidth();
h = child.getMeasuredHeight();
d = getMeasuredWidth();
}
else
{
w = child.getMeasuredWidth();
h = child.getMeasuredHeight();
d = getWidth();
}
child.setCurrentAngle(angleOffset);
// Measure child
child.measure(w, h);
int childLeft;
// Position vertically based on gravity setting
int childTop = calculateTop(child, true);
childLeft = 0;
child.layout(childLeft, childTop, w, h);
Calculate3DPosition(child, d, angleOffset);
}
</pre><br />
Let’s look at <code>trackMotionScroll </code>method in the <code>Gallery </code>class, it’s called when the widget is being scrolled or flinged and does the necessary stuff for the Gallary animation. But it moves images just by x-coordinate. To make them rotate in 3D space, we must create different functionality. We just change the current angle of an image and calculate it’s position in 3D space:<br />
<br />
<pre class="java" name="code">void trackMotionScroll(float deltaAngle) {
if (getChildCount() == 0) {
return;
}
for(int i = 0; i < getAdapter().getCount(); i++){
CarouselItem child = (CarouselItem)getAdapter().
getView(i, null, null);
float angle = child.getCurrentAngle();
angle += deltaAngle;
while(angle > 360.0f)
angle -= 360.0f;
while(angle < 0.0f)
angle += 360.0f;
child.setCurrentAngle(angle);
Calculate3DPosition(child, getWidth(), angle);
}
// Clear unused views
mRecycler.clear();
invalidate();
}
</pre>And after images were flinged or scrolled, we have to place them into the corresponding places: <br />
<pre class="java" name="code">/**
* Brings an item with nearest to 0 degrees angle to
* this angle and sets it selected
*/
private void scrollIntoSlots(){
// Nothing to do
if (getChildCount() == 0 || mSelectedChild == null) return;
// get nearest item to the 0 degrees angle
// Sort itmes and get nearest angle
float angle;
int position;
ArrayList<CarouselItem> arr = new ArrayList<CarouselItem>();
for(int i = 0; i < getAdapter().getCount(); i++)
arr.add(((CarouselItem)getAdapter().getView(i, null, null)));
Collections.sort(arr, new Comparator<CarouselItem>(){
@Override
public int compare(CarouselItem c1, CarouselItem c2) {
int a1 = (int)c1.getCurrentAngle();
if(a1 > 180)
a1 = 360 - a1;
int a2 = (int)c2.getCurrentAngle();
if(a2 > 180)
a2 = 360 - a2;
return (a1 - a2) ;
}
});
angle = arr.get(0).getCurrentAngle();
// Make it minimum to rotate
if(angle > 180.0f)
angle = -(360.0f - angle);
// Start rotation if needed
if(angle != 0.0f)
{
mFlingRunnable.startUsingDistance(-angle);
}
else
{
// Set selected position
position = arr.get(0).getIndex();
setSelectedPositionInt(position);
onFinishedMovement();
}
}
</pre>And to scroll to the definite item: <br />
<pre class="java" name="code">void scrollToChild(int i){
CarouselItem view = (CarouselItem)getAdapter().
getView(i, null, null);
float angle = view.getCurrentAngle();
if(angle == 0)
return;
if(angle > 180.0f)
angle = 360.0f - angle;
else
angle = -angle;
mFlingRunnable.startUsingDistance(angle);
}
</pre>Here’s the <code>Calculate3DPosition </code>method: <br />
<pre class="java" name="code">private void Calculate3DPosition(CarouselItem child, int diameter,
float angleOffset){
angleOffset = angleOffset * (float)(Math.PI/180.0f);
float x = - (float)(diameter/2 * Math.sin(angleOffset)) +
diameter/2 - child.getWidth()/2;
float z = diameter/2 * (1.0f - (float)Math.cos(angleOffset));
float y = - getHeight()/2 + (float) (z * Math.sin(mTheta));
child.setX(x);
child.setZ(z);
child.setY(y);
}
</pre>Some methods that don’t have a sense with 3D gallery were removed: <code>offsetChildrenLeftAndRight</code>, <code>detachOffScreenChildren</code>, <code>setSelectionToCenterChild</code>, <code>fillToGalleryLeft</code>, <code>fillToGalleryRight</code>. So, the main thing that happens with images is in <code>getChildStaticTransformation </code>method, where they are transformed in 3D space. It just takes a ready to use position from <code>CarouselImage </code>class that was calculated by <code>Calculate3DPosition </code>while flinging/scrolling and moves an image there: <br />
<pre class="java" name="code">protected boolean getChildStaticTransformation
(View child, Transformation transformation) {
transformation.clear();
transformation.setTransformationType(Transformation.TYPE_MATRIX);
// Center of the item
float centerX = (float)child.getWidth()/2,
centerY = (float)child.getHeight()/2;
// Save camera
mCamera.save();
// Translate the item to it's coordinates
final Matrix matrix = transformation.getMatrix();
mCamera.translate(((CarouselImageView)child).getX(),
((CarouselImageView)child).getY(),
((CarouselImageView)child).getZ());
// Align the item
mCamera.getMatrix(matrix);
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
// Restore camera
mCamera.restore();
return true;
}
</pre>One thing to know is that if you will just rotate images and position them in 3D space, they can overlap each other in the wrong order. For example, an image with 100.0 z-coordinate can be drawn in front of image with 50.0 z-coordinate. To resolve this trouble, we can override <code>getChildDrawingOrder</code>: <br />
<pre class="java" name="code">protected int getChildDrawingOrder(int childCount, int i) {
// Sort Carousel items by z coordinate in reverse order
ArrayList<CarouselItem> sl = new ArrayList<CarouselItem>();
for(int j = 0; j < childCount; j++)
{
CarouselItem view = (CarouselItem)getAdapter().getView(j,null, null);
if(i == 0)
view.setDrawn(false);
sl.add((CarouselItem)getAdapter().getView(j,null, null));
}
Collections.sort(sl);
// Get first undrawn item in array and get result index
int idx = 0;
for(CarouselItem civ : sl)
{
if(!civ.isDrawn())
{
civ.setDrawn(true);
idx = civ.getIndex();
break;
}
}
return idx;
}
</pre>Ok, it still has a lot to do, like bugs catching and optimization. I didn’t yet test all the functionality, but in the first approximation, it works. Icons were taken from here: [<a href="http://www.blogger.com/post-edit.g?blogID=426859432260012411&postID=6526286963047593038#icons" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">4</a>]. P.S. Fixed bug in <code>Rotator </code>class. Jerky "scroll into slots" was made more soft and fluid. Reworked the <code>Rotator </code>class. It uses only angular acceleration now. <br />
<h2>Resources</h2><ol><li><a class="anchor" href="http://ultimatefaves.com/" id="ultimatefaves" name="ultimatefaves" title="ultimatefaves"><div style="font-size:10pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">http://ultimatefaves.com/</div></a> </li>
<li><a class="anchor" href="http://www.inter-fuser.com/2010/02/android-coverflow-widget-v2.html" id="coverflow" name="coverflow" title="coverflow"><div style="font-size:10pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">http://www.inter-fuser.com/2010/02/android-coverflow-widget-v2.html</div></i></a> </li>
<li><a class="anchor" href="http://www.inter-fuser.com/2010/01/android-coverflow-widget.html" id="coverflow1" name="coverflow1" title="coverflow1"><div style="font-size:10pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">http://www.inter-fuser.com/2010/01/android-coverflow-widget.html</div></i></a> </li>
<li><a class="anchor" href="http://www.iconsmaster.com/Plush-Icons-Set/" id="icons" name="icons" title="icons"><div style="font-size:10pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">http://www.iconsmaster.com/Plush-Icons-Set/</div></i></a> </li>
</ol></div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com84tag:blogger.com,1999:blog-426859432260012411.post-88567392284152086852011-10-26T11:38:00.000+04:002011-12-27T13:19:04.289+04:00Android. ImageView with SVG Support<div dir="ltr" style="text-align: left;" trbidi="on"><br />
<a href="http://horribileru.blogspot.com/2011/10/android-imageview-svg.html"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Русский перевод</div></i></a><br />
<br />
<a href="https://code.google.com/p/image-view-svg/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Source Code</div></i></a><br />
<br />
This my article was originally published on The Code Project web site. You can see it at <a href="http://www.codeproject.com/KB/android/AndroidImageViewSVG.aspx" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">Codeproject</a>.<br />
<br />
<h2>Introduction</h2>As you know, Android doesn’t support SVG format. But benefits of SVG are obvious. First, it’s scalable. You don’t need to have pictures in different resolutions, no need to scale, for example, bitmap image with a quality loss. SVG image can be scaled to any resolution and the quality will be the same. Second, SVG is an ordinary XML file, so its size can be much lesser than the raster format file of the same picture. Moreover, you can change a picture on the fly due to this feature. You can open an SVG file in an ordinary text editor and look how it's composed. And so on… But as Android doesn’t deal with SVG, it will imply some native coding. The good news is that there won’t be a lot of native coding. There are some open source libraries for parsing and rasterizing SVG format.<br />
<br />
There are a lot of tutorials how to begin the native development for the Android platform, so I won’t repeat all of them here. I will just give some useful tips.<br />
<br />
<ul><li>First, you need the Eclipse IDE. You can download it at [<a href="#eclipse" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">1</a>]. </li>
<li>Or as an alternative, you can use Motodev Studio [<a href="#motodev" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">2</a>]. I prefer the second one as it has some delicious features. BTW, you can install Motodev Studio as a plugin for the Eclipse IDE. I couldn’t setup it on OpenSUSE, but as the plugin it works fine. </li>
<li>Once Eclipse is installed, add CDT plugin to it. </li>
<li>Add Android plugin [<a href="#adt" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">3</a>]. </li>
<li>After that, add Eclipse Sequoyah[<a href="#sequoyah" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">4</a>] plugin needed for the native debugging. But make sure you installed CDT before Sequoyah. As the Sequoyah project claims, it installs all dependencies it didn’t really install CDT in my case. While installing Sequoyah, make sure you unchecked “Group Items by Category” and checked “Sequoyah Android Native Support”. </li>
<li>Also Windows users will need cygwin[<a href="#cygwin" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">5</a>] (Add the <em>Cygwin/bin</em> path to your system. Also configure build path - set build command to something like "bash C:\AndroidNDK\ndk-build"). While installing it, setup development tools. </li>
<li>Download Android SDK [<a href="#androidsdk" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">6</a>]. </li>
<li>And at last, you will need Android NDK. Download CrystaX NDK[<a href="#crystax" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">7</a>]. It has support for C++ exceptions, RTTI and Standard C++ Library. (Current(6) Google Android NDK supports them but can't build libsvg) </li>
<li>In Eclipse preferences, set Android SDK & NDK locations. That’s all for now. </li>
</ul> For the first approach, I will use android-libsvg [<a href="#libsvgandroid" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">8</a>] library. Really it depends on libsvg, libpng, libjpeg, libexpat and zlib. But at this moment, it has support for almost all features of SVG format. To get its sources, create <em>android-libsvg</em> folder somewhere in a file system and go to this folder in console (in cygwin for windows users) and run “<code><span class="Apple-style-span" style="color: red;">bzr branch lp:libsvg-android</span></code>” command. Bazaar will download sources to this folder.<br />
<br />
Ok. Create a new Android project “<code><span class="Apple-style-span" style="color: red;">ImageViewSvg</span></code>”. Now right click on the project and go to AndroidTools/Add Native support. It will create “<em>jni</em>” folder in the project. Delete all stuff from it and copy contents of “<em>jni</em>” folder of <code><span class="Apple-style-span" style="color: red;">android-libsvg</span> </code>project. Refresh <em>jni</em> folder in the project. Let’s look at <em>Android.mk</em> file in “<em>jni</em>” folder. I'll explain some variables:<br />
<br />
<br />
<ul><li><strong>LOCAL_PATH := $(call my-dir) – my-dyr macro sets LOCAL_PATH variable used to locate source files into current directory</strong></li>
<li><strong>include $(CLEAR_VARS) – clears all local variables </strong></li>
<li><strong>LOCAL_MODULE – the name of the library </strong></li>
<li><strong>LOCAL_CFLAGS – sets compiler flags and some include directories</strong></li>
<li><strong>LIBJPEG_SOURCES, … - the list of sources files for each library that will be used </strong></li>
<li><strong>LOCAL_LDLIBS – links to additional libraries </strong></li>
<li><strong>LOCAL_SRC_FILES – the list of all source files to be compiled, here it contains all sources for all libraries</strong></li>
<li><strong>BUILD_SHARED_LIBRARY – link to mk file to build shared library </strong></li>
</ul><br />
<br />
For more information, please see <em>ANDROID-MK.TXT</em> in NDK. <br />
Sometimes on Windows, after restarting IDE, it can’t run ndk-build. Then right click the project and go to “Build Path/Configure Build Path” and change “Build command” to something like “bash /cygdrive/c/ndk/ndk-build”. <br />
<br />
Next create <code><span class="Apple-style-span" style="color: red;">com.toolkits.libsvgandroid</span> </code>package and copy there <em>SvgRaster.java</em> from the <code><span class="Apple-style-span" style="color: red;">libsvg-android</span> </code>project. Preparations are over.<br />
<br />
<br />
To make <code><span class="Apple-style-span" style="color: red;">ImageView</span> </code>class to support SVG format, it’s enough to inherit from it and override some methods. But I want it to set standard <code><span class="Apple-style-span" style="color: red;">android:src</span> </code>attribute as SVG file and to be able to pick the file from standard “<em>drawable</em>” folder vs. “<em>raw</em>” folder. At the beginning, let’s make all logic to be done in the constructor. To get access to the <code><span class="Apple-style-span" style="color: red;">android:src</span> </code>attribute, add <em>attrs.xml</em> file to the <em>res/values</em> folder:<br />
<br />
<pre class="xml" name="code"><?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ImageViewSvg">
<attr name="android:src"/>
</declare-styleable>
</resources>
</pre>Let’s look at <code><span class="Apple-style-span" style="color: red;">ImageView</span> </code>class constructor sources. It contains the following code:<br />
<br />
<pre class="java" name="code">Drawable d =
a.getDrawable(com.android.internal.R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
</pre><br />
<br />
<br />
And look at <code>setImageBitmap </code>method. It just calls <code>setImageDrawable</code>. So we can use it in our constructor if we have the corresponding bitmap. But what about getting file from “<em>drawable</em>” folder. Nothing supernatural – get resource ID from the <code>android:src </code>attribute and read raw file into an input stream. Next, <code>libandroidsvg </code>gives us a way to parse SVG file: <br />
<br />
So, the constructor will look like this:<br />
<br />
<pre class="java" name="code">public ImageViewSvg(Context context, AttributeSet attrs, int defStyle) {
// Let's try load supported by ImageView formats
super(context, attrs, defStyle);
if(this.getDrawable() == null)
{
// Get defined attributes
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.ImageViewSvg, defStyle, 0);
// Getting a file name
CharSequence cs =
a.getText(R.styleable.ImageViewSvg_android_src);
String file = cs.toString();
// Is it SVG file?
if (file.endsWith(".svg")) {
// Retrieve ID of the resource
int id =
a.getResourceId(
R.styleable.ImageViewSvg_android_src, -1);
if(id != -1){
try {
// Get the input stream for the raw resource
InputStream inStream =
getResources().openRawResource(id);
int size = inStream.available();
// Read into the buffer
byte[] buffer = new byte[size];
inStream.read(buffer);
inStream.close();
// And make a string
mSvgContent =
EncodingUtils.getString
(buffer, "UTF-8");
// Parse it
mSvgId = SvgRaster.svgAndroidCreate();
SvgRaster.svgAndroidParseBuffer
(mSvgId, mSvgContent.toString());
SvgRaster.svgAndroidSetAntialiasing(mSvgId, true);
mIsSvg = true;
} catch (IOException e) {
mIsSvg = false;
e.printStackTrace();
}
}
}
}
}
</pre>Another problem is that SVG doesn’t have a size(Not always, some have desired ones. See P.S.). It’s scalable and it’s all. Moreover <code><span class="Apple-style-span" style="color: red;">ImageView</span> </code>layout parameters can be set to <code><span class="Apple-style-span" style="color: red;">wrap_content</span></code>, <code><span class="Apple-style-span" style="color: #cc0000;">fill_parent</span> </code>or we can set predefined size of the image. But when a layout is required, Android sets the size and we can override <code><span class="Apple-style-span" style="color: red;">onSizeChanged</span> </code>method. The only problem is <code><span class="Apple-style-span" style="color: red;">wrap_content</span> </code>attribute. In this case, the size will be <code>0</code>. The idea is to replace <code><span class="Apple-style-span" style="color: red;">wrap_content</span> </code>with <code><span class="Apple-style-span" style="color: red;">fill_parent</span> </code>on the fly. But doing it in the constructor will give nothing. If you debug through source codes, you will see that parent layout drags layout parameters from attributes directly and calls <code><span class="Apple-style-span" style="color: red;">setLayoutParams</span> </code>method. Let’s override it:<br />
<br />
<pre class="java" name="code">@Override
public void setLayoutParams(ViewGroup.LayoutParams params){
if(mIsSvg)
{
// replace WRAP_CONTENT if needed
if(params.width == ViewGroup.LayoutParams.WRAP_CONTENT
&& getSuggestedMinimumWidth() == 0)
params.width = ViewGroup.LayoutParams.FILL_PARENT;
if(params.height == ViewGroup.LayoutParams.WRAP_CONTENT
&& getSuggestedMinimumHeight() == 0)
params.height = ViewGroup.LayoutParams.FILL_PARENT;
}
super.setLayoutParams(params);
}
</pre>And <code><span class="Apple-style-span" style="color: red;">onSizeChanged</span></code>:<br />
<br />
<pre class="java" name="code">@Override
public void onSizeChanged(int w, int h, int ow, int oh){
if(mIsSvg){
//Create the bitmap to raster svg to
Canvas canvas = new Canvas();
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
canvas.setBitmap(mBitmap);
// Render SVG with use of libandroidsvg
SvgRaster.svgAndroidRenderToArea(
mSvgId, canvas,
0, 0, canvas.getWidth(), canvas.getHeight());
this.setImageBitmap(mBitmap);
}
else
super.onSizeChanged(w, h, ow, oh);
}
</pre><br />
<br />
And at last, it’s the time to try it. Create the following layout:<br />
<br />
<pre class="xml" name="code"><?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="#AA0000"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:layout_weight="1.0"
android:gravity="center"
>
<com.imageviewsvg.controls.ImageViewSvg
android:src="@drawable/lion"
android:layout_width="100dip"
android:layout_height="100dip"
android:id="@+id/svgview"
android:layout_gravity="center"
/>
</LinearLayout>
</pre><br />
And run:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjS-4QKzd1hjUqJrKTIHKtEyo3BB-lFv4a9vubSJCZFgC-xn7StuTGruUy8BMK34NZl_DQzeHGkeq_UejxfiQcMNa2V3jaKDL8gU6MR7eOnU7h9BjaUxWdeaO2sXsVXEsunLHKiVoYlvoY-/s1600/pic1.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjS-4QKzd1hjUqJrKTIHKtEyo3BB-lFv4a9vubSJCZFgC-xn7StuTGruUy8BMK34NZl_DQzeHGkeq_UejxfiQcMNa2V3jaKDL8gU6MR7eOnU7h9BjaUxWdeaO2sXsVXEsunLHKiVoYlvoY-/s320/pic1.JPG" width="269" /></a></div><br />
<br />
<br />
<h2>Debugging Native Code </h2>To debug native code, you can follow the instructions given by Carlos Souto at Sequoyah Project [<a href="#debug" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">9</a>]. But during the first time, some things are unclear. So there are several tips: <br />
<ul><li>While configuring C++ debug configuration, the application really must be <code><span class="Apple-style-span" style="color: red;">app_process</span></code>, don’t care Eclipse says the program doesn’t exist, it will be created later. </li>
<li>You should run ndk-gdb each time you run debugging. Sometimes the command should be ndk-gdb –adb=<path-to-sdk>/tools/adb –force.</path-to-sdk></li>
<li>Don’t forget to set "debuggable" in the manifest. </li>
</ul><h2>Second Approach. Anti Grain Geometry. </h2>There is another library where you can use to rasterize SVG files. It’s Anti Grain Geometry[<a href="#antigrain" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">10</a>]. The only extra library that will be needed is <code><span class="Apple-style-span" style="color: red;">libexpat</span></code>. We already have it in our project. In the <em>jni</em> folder, create folders like this:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNgnIBQXRcZ0oINpBRxk5Jr7gVkYgkYiqx-HcPsgvOxOpeVYuOXYmdzXCaTE98Eo-SDsDBh0kvHeZvnNX6VCmJHLsVn1ertijMJWy-jPM3KSHavVrXcWBvEseElT9alKbZog6tse_uY6FA/s1600/pic2.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNgnIBQXRcZ0oINpBRxk5Jr7gVkYgkYiqx-HcPsgvOxOpeVYuOXYmdzXCaTE98Eo-SDsDBh0kvHeZvnNX6VCmJHLsVn1ertijMJWy-jPM3KSHavVrXcWBvEseElT9alKbZog6tse_uY6FA/s1600/pic2.JPG" /></a></div><br />
<br />
Copy the corresponding files from agg sources folder into <em>gpc/include/src</em> folders. There in an <em>examples</em> folder where you can find <em>svg_viewer</em> folder. Copy all files except <em>svg_test</em> into <em>aggsvg jni</em> folder. It will parse and rasterize SVG with use of AGG. But it has only basic support for SVG and can’t parse complicated stuff. You should extend the parser by yourself. In aggsvg-android folder, create <em>aggsvgandroid.cpp</em> file. The example parses SVG from a file system. To parse a <code>string</code>, add the following method to a <code><span class="Apple-style-span" style="color: red;">parser</span> </code>class:<br />
<br />
<pre class="cpp" name="code">void parser::parse(const char *chars, int length){
char msg[1024];
XML_Parser p = XML_ParserCreate(NULL);
if(p == 0)
{
throw exception("Couldn't allocate memory for parser");
}
XML_SetParamEntityParsing(p, XML_PARAM_ENTITY_PARSING_ALWAYS);
XML_UseForeignDTD(p, true);
XML_SetUserData(p, this);
XML_SetElementHandler(p, start_element, end_element);
XML_SetCharacterDataHandler(p, content);
int done = 0;
std::string str = std::string(chars);
std::istringstream inputString(str);
while(true){
if(done)
break;
size_t len = inputString.readsome(m_buf, buf_size);
done = len < buf_size;
if(!XML_Parse(p, m_buf, len, done))
{
sprintf(msg,
"%s at line %d\n",
XML_ErrorString(XML_GetErrorCode(p)),
(int)XML_GetCurrentLineNumber(p));
throw exception(msg);
}
}
XML_ParserFree(p);
char* ts = m_title;
while(*ts)
{
if(*ts < ' ') *ts = ' ';
++ts;
}
}
</pre><br />
At the end of <em>Android.mk</em> file, add section to build another <br />
library. It’s pretty simple. Just clear variables after the first <br />
library build and set them to build another library. And here is the <br />
class to rasterize with use of AGG:<br />
<br />
<pre class="cpp" name="code">class SvgRasterizer{
agg::svg::path_renderer m_path;
double m_min_x;
double m_min_y;
double m_max_x;
double m_max_y;
double m_x;
double m_y;
pix_format_e pixformat;
agg::rendering_buffer m_rbuf_window;
public:
SvgRasterizer(pix_format_e format, uint32_t width,
uint32_t height, void *pixels) : \
m_path(), \
m_min_x(0.0), \
m_min_y(0.0), \
m_max_x(0.0), \
m_max_y(0.0), \
pixformat(format)
{
m_rbuf_window.attach((unsigned char*)pixels, width, height, 4*width);
}
void parse_svg(const char* svg, int length){
// Create parser
agg::svg::parser p(m_path);
// Parse SVG
p.parse(svg, length);
// Make all polygons CCW-oriented
m_path.arrange_orientations();
// Get bounds of the image defined in SVG
m_path.bounding_rect(&m_min_x, &m_min_y, &m_max_x, &m_max_y);
}
void rasterize_svg()
{
typedef agg::pixfmt_rgba32 pixfmt;
typedef agg::renderer_base<pixfmt> renderer_base;
typedef agg::renderer_scanline_aa_solid<renderer_base> renderer_solid;
pixfmt pixf(m_rbuf_window);
renderer_base rb(pixf);
renderer_solid ren(rb);
agg::rasterizer_scanline_aa<> ras;
agg::scanline_p8 sl;
agg::trans_affine mtx;
double scl;
// Calculate the scale the image to fit given bitmap
if(m_max_y > m_max_x)
scl = pixf.height()/m_max_y;
else
scl = pixf.width()/m_max_x;
// Default gamma as is
ras.gamma(agg::gamma_power(1.0));
mtx *= agg::trans_affine_scaling(scl);
m_path.expand(0.0);
// Render image
m_path.render(ras, sl, ren, mtx, rb.clip_box(), 1.0);
ras.gamma(agg::gamma_none());
}
};
</pre><br />
And in sources, I’ve added an ability to test both approaches:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEGURKS9q8sfQYhqw-lkr8qIOKYopDRKc7qdZULc7wPu7Tv7A-sUwSWFLJDbLk4wzrsFqdcfhlLUU95oixpEwdzB-nzISI-L9aXoezshR6WkMDYf7AiaqM29ZbUB_QFZSMyNLcjZVKfPEC/s1600/pic3.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEGURKS9q8sfQYhqw-lkr8qIOKYopDRKc7qdZULc7wPu7Tv7A-sUwSWFLJDbLk4wzrsFqdcfhlLUU95oixpEwdzB-nzISI-L9aXoezshR6WkMDYf7AiaqM29ZbUB_QFZSMyNLcjZVKfPEC/s320/pic3.JPG" width="256" /></a></div><br />
<br />
<br />
<h2>Conclusion </h2>So, there are at last two ways to show SVG file in Android. The main benefit of <code>libsvg-android </code>is that it is ready to use, but it is more than three times slower than AGG, with which you should extend SVG parser by your own. Also with AGG, you get extra features for the image processing. I’ve just used <code>ImageView </code>in the layout, but to use it programmatically you, of cause, should override more methods, such as <code>setImageResource </code>for example. <br />
<br />
That’s all. Thanks!<br />
<br />
<br />
<h2>Resources </h2><ol><li><a class="anchor" href="http://www.eclipse.org/" id="eclipse" name="eclipse" title="eclipse" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://www.eclipse.org</a> </li>
<li><a class="anchor" href="http://developer.motorola.com/docstools/motodevstudio/" id="motodev" name="motodev" title="motodev" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://developer.motorola.com/docstools/motodevstudio</a> </li>
<li><a class="anchor" href="http://developer.android.com/guide/developing/tools/adt.html" id="motodevstudio" name="adt" title="adt" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://developer.android.com/guide/developing/tools/adt.html </a></li>
<li><a class="anchor" href="http://www.eclipse.org/sequoyah/" id="sequoyah" name="sequoyah" title="sequoyah" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://www.eclipse.org/sequoyah/</a> </li>
<li><a class="anchor" href="http://www.cygwin.com/" id="cygwin" name="cygwin" title="cygwin" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://www.cygwin.com/</a> </li>
<li><a class="anchor" href="http://developer.android.com/sdk/index.html" id="androidsdk" name="androidsdk" title="androidsdk" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://developer.android.com/sdk/index.html</a> </li>
<li><a class="anchor" href="http://www.crystax.net/en/android/ndk/6" id="crystax" name="crystax" title="crystax" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://www.crystax.net/en/android/ndk/6</a> </li>
<li><a class="anchor" href="https://launchpad.net/libsvg-android" id="libsvgandroid" name="libsvgandroid" title="libsvgandroid" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">https://launchpad.net/libsvg-android</a> </li>
<li><a class="anchor" href="http://www.eclipse.org/sequoyah/documentation/native_debug.php" id="debug" name="debug" title="debug" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://www.eclipse.org/sequoyah/documentation/native_debug.php</a> </li>
<li><a class="anchor" href="http://www.antigrain.com/" id="antigrain" name="antigrain" title="antigrain" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">http://www.antigrain.com/</a> </li>
</ol><br />
<br />
<br />
P.S. Also there is the article in Code Project with some additional improvements for SVG usage:<br />
<a href="http://www.codeproject.com/KB/android/Android_SVG_support.aspx" style="font-size:10pt; color:#ff0000; filter:Glow(color=#ffffff, strength=12)">Drawable with SVG Support</a><br />
<br />
Known issues:<br />
<br />
ibsvg-android doesn't support rgb(255, 255, 255) color representation (used by Inkscape). (At least at the moment I wrote the article.)<br />
Check if there are occurences of such representation and use fill: #ffffff<br />
<br />
Also, there may be some other unsupported features unsupported by libsvg-android.<br />
Check the structure of the .svg file you're using.<br />
<br />
<br />
</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com2tag:blogger.com,1999:blog-426859432260012411.post-25747798027721365782011-10-25T16:28:00.000+04:002011-12-27T13:20:26.133+04:00Animated Splash<div dir="ltr" style="text-align: left;" trbidi="on"><br />
<h1 class="fn" id="ctl00_TitleArea_ArticleTitle" style="text-align: center;">An Advanced Splash Screen for Android App</h1><div><a href="http://horribileru.blogspot.com/2011/10/blog-post.html"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Русский перевод</div></i></a><br />
<br />
This my article was originally published on The Code Project web site. You can see it at <a href="http://www.codeproject.com/KB/android/AndroidSplash.aspx" style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Codeproject</a>.<br />
<br />
<a href="https://code.google.com/p/android-tips-demo/"><i><div style="width:100%; font-size:12pt; color:#ffffff; filter:Glow(color=#ff0000, strength=12)">Source Code</div></i></a><br />
<h2>Introduction </h2><div><br />
</div> Everyone wants his/her application to be beautiful and attractive for an eye of a user. And there are a lot of applications, at least desktop applications, mostly games that use splash screens. It’s nice and, moreover, while the splash screen is working, you can initialize your application. Many tutorials exist explaining how to begin Android programming and I won't repeat them here. You can find them all over the internet. So I will show only the programming stuff.<br />
<br />
<br />
<h2>The Beginning</h2><br />
Create a new Android Eclipse project with the following settings:<br />
<br />
<pre class="python" name="code">Project name : AdvancedSplashDemo
Build target: I've set it to Android 2.1
Application name: Advanced Splash Demo
Package name: Advanced Splash Demo
Create Activity: MainActivity – it will be the application itself
</pre><br />
So, as we don’t need a splash screen after it’s done, the first thought is to use another activity that will start the main activity and silently die after that. Let’s create a layout for the splash – it will be a linear layout and an Image View inside it. Create a new Android XML file "<i>splash.xml</i>" in <i>appfolder/res/layout folder</i>. Don’t make it fill parent, as we want it to be really as splash screen. The image view has to wrap a content too: <br />
<br />
<pre class="xml" name="code">
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/TheSplashLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" >
<ImageView
android:id="@+id/SplashImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" >
</ImageView>
</LinearLayout>
</pre><br />
Here the gravity attribute value is set to "<span class="Apple-style-span" style="color: red;">center</span>" to make the splash to be at the center of the screen. Add some picture to the <i>appfolder/res/drawable</i> folder and press F5 on the project. I’ve added <i>lnxins.png</i> and as you can see, set it as the image view source. <br />
<br />
So far, let’s look at the application manifest. It has now just one “<span class="Apple-style-span" style="color: red;">.MainActivity</span>” activity set as a launcher. We’ll set it as default category and add another splash activity with a splash layout and will set it as the launcher. Open the manifest and open an application tab. For the main activity, change an Android intent category to default. Near application nodes press an “Add…” button, choose create a new element at top level and double click on Activity. For new activity, click on a “<span class="Apple-style-span" style="color: red;">Name*</span>” hyperlink and enter “<span class="Apple-style-span" style="color: red;">SplashScreen</span>” class. In sources, a new class will be added for the splash activity. Next, press the “<span class="Apple-style-span" style="color: red;">Add…</span>” button again for the SplashScreen node and add the intent filter. Again, for just added intent filter, add an action and a category.<br />
<br />
Set the action to <span class="Apple-style-span" style="color: red;">android.intent.action.MAIN</span> and the category to <span class="Apple-style-span" style="color: red;">android.intent.category.LAUNCHER</span>. So the Splash screen activity will be run first. The manifest should look like the following:<br />
<br />
<pre class="xml" name="code"><?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yourname.main"
android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity android:name="SplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN"></action>
<category android:name="android.intent.category.LAUNCHER"></category>
</intent-filter>
</activity>
</application>
</manifest>
</pre><br />
<h2>A Little Coding</h2>Open <i>SplashScreen.java</i> class. Now it has overridden <code><span class="Apple-style-span" style="color: red;">onCreate</span> </code>method only. Override <code><span class="Apple-style-span" style="color: red;">onTouchEvent</span> </code>method to give the user a possibility to close splash screen at every moment. And don’t forget synchronization or you will have random crashes. Here’s the class code:<br />
<br />
<pre class="java" name="code">public class SplashScreen extends Activity {
/**
* The thread to process splash screen events
*/
private Thread mSplashThread;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Splash screen view
setContentView(R.layout.splash);
final SplashScreen sPlashScreen = this;
// The thread to wait for splash screen events
mSplashThread = new Thread(){
@Override
public void run(){
try {
synchronized(this){
// Wait given period of time or exit on touch
wait(5000);
}
}
catch(InterruptedException ex){
}
finish();
// Run next activity
Intent intent = new Intent();
intent.setClass(sPlashScreen, MainActivity.class);
startActivity(intent);
stop();
}
};
mSplashThread.start();
}
/**
* Processes splash screen touch events
*/
@Override
public boolean onTouchEvent(MotionEvent evt)
{
if(evt.getAction() == MotionEvent.ACTION_DOWN)
{
synchronized(mSplashThread){
mSplashThread.notifyAll();
}
}
return true;
}
}
</pre><br />
<h2>A Little Beautification</h2>First, let’s make the splash screen transparent. In <em>appfolder/res/values</em>, add new Android XML file <em>styles.xml</em> and add to it a transparent theme:<br />
<br />
<pre class="xml" name="code"><resources>
<style name="Theme.Transparent" parent="android:Theme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>
</pre><br />
<br />
Here are some explanations: as you can see, the style’s parent is <code><span class="Apple-style-span" style="color: red;">android:Theme</span> </code>so we could apply it to our activity. And as you can see, the attribute's names are clear and you can understand what they mean.<br />
Next, we’ll apply this theme to our splash. In the manifest file for the splash activity, set a "<code><span class="Apple-style-span" style="color: red;">theme</span></code>" attribute to the just created theme:<br />
<br />
<pre class="xml" name="code"><activity
android:name="SplashScreen"
android:theme="@style/Theme.Transparent"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"></action>
<category android:name="android.intent.category.LAUNCHER"></category>
</intent-filter>
</activity>
</pre><br />
Let’s suppose we’re developing a game application. And gamers don’t like when something distracts them from the game process. Most of them prefer fullscreen mode. So, set the fullscreen theme for the main activity:<br />
<br />
<pre class="xml" name="code"><activity android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
</pre><br />
Run it. Looks better. Now we’ll make it fade-in and fade-out. Create in <i>appfolder/res</i> folder new folder "<span class="Apple-style-span" style="color: red;">anim</span>" and add to it two Android XML files – <i>appear.xml</i> and <i>disappear.xml</i>. They will be alpha animations.<br />
<br />
<h4>Appear.xml</h4><div><br />
</div><br />
<pre class="xml" name="code"><set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:interpolator="@android:anim/accelerate_interpolator"
android:fromAlpha="0.0" android:toAlpha="1.0"
android:duration="800"
/>
</set>
</pre><br />
<h4>Disappear.xml</h4><div><br />
</div><br />
<pre class="xml" name="code"><set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1.0" android:toAlpha="0.0"
android:duration="800"
/>
</set>
</pre>In these animations, they just change the alpha channel of an object from <code><span class="Apple-style-span" style="color: red;">fromAlpha</span> </code>value to <code><span class="Apple-style-span" style="color: red;">toAlpha</span> </code>value for a given period of time. Now add new style in <em>styles.xml</em>:<br />
<br />
<pre class="xml" name="code"><style name="Animations" parent="@android:Animation" />
<style name="Animations.SplashScreen">
<item name="android:windowEnterAnimation">@anim/appear</item>
<item name="android:windowExitAnimation">@anim/disappear</item>
</style>
</style>
</pre>So, on the window, enter the “<code><span class="Apple-style-span" style="color: red;">appear</span></code>” animation will be performed and on the window exit the “<code><span class="Apple-style-span" style="color: red;">disappear</span></code>” animation will be performed. Add this style to <code><span class="Apple-style-span" style="color: orange;">Theme.Transparent</span> </code>theme:<br />
<br />
<pre class="xml" name="code"><style name="Theme.Transparent" parent="android:Theme">
………
<item name="android:windowAnimationStyle">
@style/Animations.SplashScreen
</item>
</style>
</pre><br />
Ok, it’s time to run it again. Now it looks nice. And more…<br />
<h2>Don’t Shoot at a Programmer, He Draws As He Can… </h2>Let’s create an animated splash screen. As an artist, I’m not very good so I used Gimp’s Script-Fu to generate a set of frames for animations. First, remove <code><span class="Apple-style-span" style="color: red;">android:src</span> </code>attribute in <em>splash.xml</em>. Then, in the drawable folder, create <em>flag.xml</em>:<br />
<br />
<pre class="xml" name="code"><?xml version="1.0" encoding="utf-8"?>
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/flaganim"
android:oneshot="false"
>
<item android:drawable="@drawable/f03" android:duration="100" />
<item android:drawable="@drawable/f04" android:duration="100" />
<item android:drawable="@drawable/f05" android:duration="100" />
<item android:drawable="@drawable/f06" android:duration="100" />
<item android:drawable="@drawable/f07" android:duration="100" />
<item android:drawable="@drawable/f08" android:duration="100" />
<item android:drawable="@drawable/f09" android:duration="100" />
<item android:drawable="@drawable/f10" android:duration="100" />
</animation-list>
</pre>Here is a set of frames and “<code><span class="Apple-style-span" style="color: red;">oneshot</span></code>” attribute says to loop them. To run animation, we need to change the splash screen class code. To the <code><span class="Apple-style-span" style="color: red;">onCreate</span> </code>method, add the following:<br />
<br />
<pre class="java" name="code">final ImageView splashImageView =
(ImageView) findViewById(R.id.SplashImageView);
splashImageView.setBackgroundResource(R.drawable.flag);
final AnimationDrawable frameAnimation =
(AnimationDrawable)splashImageView.getBackground();
</pre><br />
We've set the animation for the splash, but here’s a little problem. We can’t start it from the <span class="Apple-style-span" style="color: red;">onCreate</span> method. The animation must be started from GUI thread. For this, we’ll use a “<span class="Apple-style-span" style="color: red;">post</span>” method of the <span class="Apple-style-span" style="color: red;">ImageView</span> class. It will add our runnable to a message queue and when GUI thread will be available, it will start it:<br />
<br />
<pre class="java" name="code">splashImageView.post(new Runnable(){
@Override
public void run() {
frameAnimation.start();
}
});
</pre><br />
And here we are: <br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaxszZ-3kfZ4QpjlQB3vsAbjiUcHWaMdyJCRcqULTPvKfqrp2c2eeqcbKZHaEekHftuFx8AlrFcbbTYdhFWkH7eDalmEdQ6ZYK0_42UReu1YRMc10GzCS_qSIPXpEVCfBZQ0kjGA8QkQJ9/s1600/shot.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgaxszZ-3kfZ4QpjlQB3vsAbjiUcHWaMdyJCRcqULTPvKfqrp2c2eeqcbKZHaEekHftuFx8AlrFcbbTYdhFWkH7eDalmEdQ6ZYK0_42UReu1YRMc10GzCS_qSIPXpEVCfBZQ0kjGA8QkQJ9/s320/shot.jpg" width="306" /></a></div><br />
That’s all. Have fun with Android programming.<br />
<br />
Thanks!<br />
<br />
</div></div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com8