tag:blogger.com,1999:blog-72759458559600055952024-03-05T07:36:49.096+03:00Android. Советы.Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.comBlogger10125tag:blogger.com,1999:blog-7275945855960005595.post-46778326309245630612011-12-13T11:24:00.000+04:002011-12-13T11:24:02.057+04:00Исходники<div dir="ltr" style="text-align: left;" trbidi="on">
Исходники ко всем статьям перенесены на Google Code.
Линки к статьям:
3D Carousel демо:<br/>
<a href="https://code.google.com/p/android-carousel-demo/">3D Carousel</a>
<br/>
<br/>
ImageView с поддержкой SVG:<br/>
<a href="https://code.google.com/p/image-view-svg/">SVG ImageView</a>
<br/>
<br/>
Остальные статьи объединены в один проект:<br/>
<a href="https://code.google.com/p/android-tips-demo/">Android.Tips</a>
<br/>
<br/>
Линки в статьях также обновлены.</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com1tag:blogger.com,1999:blog-7275945855960005595.post-42837295408362378552011-12-06T13:32:00.001+04:002011-12-13T11:08:04.491+04:00Использование маски с TextEdit<div dir="ltr" style="text-align: left;" trbidi="on">
<br />
<br />
<a href="https://code.google.com/p/android-tips-demo/">Source Code</a><br />
<a href="http://horribile.blogspot.com/2011/12/using-mask-with-edittext.html">English translation</a><br />
<br />
В продолжение темы о фоорматировании текста при помощи регулярных врыжений мы<br />
разработаем функциональность для наложения маски на EditText.<br />
<br />
Конечно, мы можем воспользоваться InputType, но хотелось бы иметь более гибкую функцтональность.<br />
<br />
<br />
Для начала посмотрим на класс MaskFormatter в Swing.<br />
Он делает все, что нам надо. Изменим кое-что в этом классе.<br />
Оставим внутренние классы как есть. Но MaskFormatter работает<br />
с JFormattedTextField. Необходимо удалить весь функционал, связанный с этим контролом.<br />
В результате, мы имеем что-то типа этого:
<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 />
И чтобы упростить жизнь создадим класс TextWatcher для работы с форматтером:<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 />
Теперь мы можем работать с масками:<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/AVvXsEhXhz-_ypjxnoq9DdSN2O_U7_39oafYunZyc4QgbAX1BHJIVA0EVOWe7NgeScxvPTrhBUQl8tpsZIfY6Ap7puTa3lrpg0YqtHwAF-O6aCWBvjiIp9YOeTaIzvOgWKH-G0JcOegHUjERWC2b/s1600/msk.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhXhz-_ypjxnoq9DdSN2O_U7_39oafYunZyc4QgbAX1BHJIVA0EVOWe7NgeScxvPTrhBUQl8tpsZIfY6Ap7puTa3lrpg0YqtHwAF-O6aCWBvjiIp9YOeTaIzvOgWKH-G0JcOegHUjERWC2b/s1600/msk.png" /></a></div>
<br />
<br />
<br /></div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-7275945855960005595.post-87544170320457380152011-12-05T08:51:00.003+04:002011-12-13T11:08:18.074+04:00Форматирование EditText при помощи регулярных выражений<div dir="ltr" style="text-align: left;" trbidi="on"><a href="https://code.google.com/p/android-tips-demo/">Source Code</a><br />
<a href="http://horribile.blogspot.com/2011/11/formatting-edittext-input-with-regular.html">English translation</a><br />
<br />
Это будет короткая статья. Посмотрим как форматировать EditText при помощи регулярных выражений.<br />
<br />
Класс для форматирования наследуем от 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 />
Трюк состоит в том, что если введенный текст не соответствует паттерну, то он может соответствовать ему частично.<br />
Если это так, то мы позволяем ввод текста.<br />
<br />
И, как пример, форматирование номера телефона:<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 />
В итоге: <br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgXmT7LXi9vPGPmoueGqOBdDrElblWKKmDegeXbaUfFGkcB9IOc6wML5ImbYs0j_Gy54gxoQxQvc9-ROBx49OjJhN2_3nYNBz3w-46tfIsevdA4CAGGSonW6u9OkyRq9GreN7jL_qa-tD3U/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/AVvXsEgXmT7LXi9vPGPmoueGqOBdDrElblWKKmDegeXbaUfFGkcB9IOc6wML5ImbYs0j_Gy54gxoQxQvc9-ROBx49OjJhN2_3nYNBz3w-46tfIsevdA4CAGGSonW6u9OkyRq9GreN7jL_qa-tD3U/s320/regformatting.png" width="192" /></a></div><br />
<br />
<br />
</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com1tag:blogger.com,1999:blog-7275945855960005595.post-85392945695677262442011-11-30T16:27:00.000+04:002011-11-30T16:27:58.583+04:00Баг в 3D карусели. Android 3.0+В классе "CarouselItem" есть метод "getMatrix". Но начиная с API level 11 такой же метод есть в классе View. Необходимо переименовать этот метод во что-нибудь еще в классе "CarouselItem" и в вызове метода "pointToPosition" в классе "CarouselSpinner". <br />
<br />
Как будет время я обновлю код.Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-7275945855960005595.post-16466592934713914952011-11-30T13:03:00.000+04:002011-11-30T13:03:30.009+04:00ARM выпустило бесплатную версию Development StudioARM выпустило бесплатную версию Development Studio. Студия работает с существующим NDK, предоставляет низкоуровневый доступ на ARM устройствах — для разработки высокопроизводительных приложений. Соответственно, это обозначает, что приложения могут быть написаны на С или С++... <br />
<br />
Круто. Стоит попробовать...Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-7275945855960005595.post-20188442391799208762011-11-18T12:03:00.001+04:002011-12-13T11:08:31.145+04:00Две активности на экране в одно и то же время<div dir="ltr" style="text-align: left;" trbidi="on"><a href="https://code.google.com/p/android-tips-demo/">Source code</a><br />
<a href="http://horribile.blogspot.com/2011/11/two-activities-on-screen-at-same-time.html">English translation</a><br />
<br />
Возможно вы видели, что некоторые приложения, такие как winamp, Gimp и так далее имеют несколко раздельных окон. Мне было интересно, возможно ли такое же сделать на Android. Конечно, это можно сделать с использованием лэйаутов и прозрачных тем. Но мы не ищем легких путей.<br />
<br />
Сначала определим стили:<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 />
"<span class="Apple-style-span" style="color: red;">Theme.Transparent</span>" стиль, наподобие того, что бы использован для заставки, но без "<span class="Apple-style-span" style="color: red;">windowIsFloating</span>". Благодаря этому первая "activity" заполняет весь экран. Для второй "activity": "<span class="Apple-style-span" style="color: red;">Theme.Transparent.Floating</span>". Т.о. вторая не заполнит весь экран и прикосновения к экрану будут доступны первой. Но нет. По умолчанию активности модальны. И прикосновения не будут доступны. Небольшие изменения:<br />
<br />
<pre class="java" name="code">getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
</pre><br />
Теперь мы видим экран устройства, две активности и первая из них доступна для прикосновений.<br />
Есть еще одна вещь - взаимодействие между ними. При обычном раскладе мы используем<br />
startActivityForResult. Но в нашем случае это непременимо. Простейший путь - использовать ресивер:<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 />
В результате:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMUKfU0iyuuLABLjZf4Ghm3JtGzP8bw-KlMG0a0MsmDKexHTOGA4qhhp8kZrROxIF7j6lIA68zYvef2LysHj0cvyAYr_VKuxbAAcjG5gbEpAnDSZmUPJYf676oKdDjgP5a_YT_0mRBVzQw/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/AVvXsEiMUKfU0iyuuLABLjZf4Ghm3JtGzP8bw-KlMG0a0MsmDKexHTOGA4qhhp8kZrROxIF7j6lIA68zYvef2LysHj0cvyAYr_VKuxbAAcjG5gbEpAnDSZmUPJYf676oKdDjgP5a_YT_0mRBVzQw/s320/taos.png" width="192" /></a></div><br />
<br />
<br />
Возможно для чего-нибудь будет полезно.<br />
<br />
<br />
</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-7275945855960005595.post-47790550142491938892011-11-03T08:44:00.000+04:002011-12-13T11:08:48.682+04:00"Error" диалог в стиле iPhone. Только XML.<div dir="ltr" style="text-align: left;" trbidi="on"><a href="https://code.google.com/p/android-tips-demo/">Source Code</a><br />
<br />
<a href="http://horribile.blogspot.com/2011/11/android-iphone-style-dialog-xml-only.html">English translation</a><br />
<br />
В качестве следующего упражнения, я покажу как сделать диалог, похожий на iPhone AlertView.<br />
Мы не будем использовать для этого какие-либо файлы с картинками. Только XML.<br />
<br />
В действительности это не сложно. Для начала создадим define xml-drawable для кнопки:<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 />
Здесь у нас два слоя. Первый слой – прямоугольник с градиентом. Второй слой – прямоугольник, сдвинутый вверх на 20dip. Этот слой должен перекрывать половину первого слоя. В итоге, реальная кнопка должна быть высотой в 40dip .<br />
<br />
Теперь определим содержимое диалога – заголовок, сообщение и кнопка OK:<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 />
И последнее - фон диалога будет определен в коде. На нужен будет "drawable" с тремя слоями – прямоугольник с белой окантовкой, прямоугольник с основным фоновым цветом и прямоугольник с глянцем.<br />
Для того, чтобы белая окантовка была видна, в первом слое устанавливаем отступы:<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 />
Более сложные вещи - в классе GlossDrawable. Перегружаем метод onDraw для расчета эффекта глянца.<br />
<br />
Картинка, описывающая расчеты:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihUyeAXlF_GwgrAHaDQxoXeyJ__2IBdtKRuw2FQlLb3PQ-EHxJCfi9-CApYlG3cYU9jrA_lBEyEgjOjTnKnOi5v3fPOkPbwG0_zUUFR7JbJPFV-UPZvcgo7VxLf18tkQFK9QhPRuKnUB58/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/AVvXsEihUyeAXlF_GwgrAHaDQxoXeyJ__2IBdtKRuw2FQlLb3PQ-EHxJCfi9-CApYlG3cYU9jrA_lBEyEgjOjTnKnOi5v3fPOkPbwG0_zUUFR7JbJPFV-UPZvcgo7VxLf18tkQFK9QhPRuKnUB58/s320/ris.PNG" width="320" /></a></div><br />
<br />
По теореме Пифагора находим стороны треугольника:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnRbqhwnwGuDQ-yieWKIiX65Oeag0T9byknxB45NMB3-AeH7W75fASijXCSXW4JTBKeMQz2GN4c0caqpuhu30BMjD-vPKh_ydd2ftC-dY-PKwfJRspSnCAnP8U6SFFea2Px1AxighZZIn2/s1600/sides.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnRbqhwnwGuDQ-yieWKIiX65Oeag0T9byknxB45NMB3-AeH7W75fASijXCSXW4JTBKeMQz2GN4c0caqpuhu30BMjD-vPKh_ydd2ftC-dY-PKwfJRspSnCAnP8U6SFFea2Px1AxighZZIn2/s1600/sides.PNG" /></a></div>Далее, используя формулу Герона, находим площадь треугольника:<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/AVvXsEjIz7IN6CdTGuu4rgQCrIK_Ty7U788IBWsGsti8_qiwvxAuJQxjc0MfjUxZvYpHPr3JdGazalKPfuPX4_n040RXlQ-wNmeVEoVVd6KYHZ1xHpX8HOnQB1MVJcQka_BrJoe6zTQgaxq1BC1i/s1600/area.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjIz7IN6CdTGuu4rgQCrIK_Ty7U788IBWsGsti8_qiwvxAuJQxjc0MfjUxZvYpHPr3JdGazalKPfuPX4_n040RXlQ-wNmeVEoVVd6KYHZ1xHpX8HOnQB1MVJcQka_BrJoe6zTQgaxq1BC1i/s1600/area.PNG" /></a></div><br />
И радиус описанной окружности:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiO8LhfX8-2j3uVA6ZZTEdwbcxER-Zgc3fM_O4cyf_eIU6ZVaI0_c0E-NokAcuOH89fwgH-YKUMwbFe3K3NfKg1H2vKO5gdzX-kV8WjaMfJPqNTOXsKNcF7Fx91Y0AthZ9cB6zQg2PR5_w6/s1600/rad.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiO8LhfX8-2j3uVA6ZZTEdwbcxER-Zgc3fM_O4cyf_eIU6ZVaI0_c0E-NokAcuOH89fwgH-YKUMwbFe3K3NfKg1H2vKO5gdzX-kV8WjaMfJPqNTOXsKNcF7Fx91Y0AthZ9cB6zQg2PR5_w6/s1600/rad.PNG" /></a></div><br />
Теперь рисуем окружность чуть ниже (на 1/8 высоты прямоугольника). Центр окружности:<br />
<br />
<br />
<pre class="java" name="code">int centerX = (int) shape.getWidth() / 2;
int centerY = (int) (-radius + shape.getHeight() / 2);
</pre><br />
Прямоугольник для отрисовки окружности:<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 />
И, применяя градиент, получаем:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpvzjmXNyi_n4TlQrzBMGzpPccXq7wD7-5Lro466zwvYWfhEUerFi2j98o06gUFWiyzTeaZRfvP6dICdL4k9JI8nyRM3-gLNhALGdeqSrwtA3HTulyMtBplD3Kx3kdYkFLP78xuDIreJTj/s1600/dlg.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgpvzjmXNyi_n4TlQrzBMGzpPccXq7wD7-5Lro466zwvYWfhEUerFi2j98o06gUFWiyzTeaZRfvP6dICdL4k9JI8nyRM3-gLNhALGdeqSrwtA3HTulyMtBplD3Kx3kdYkFLP78xuDIreJTj/s1600/dlg.png" /></a></div><br />
Используйте эту же технику для создания "info" и "confirm" диалогов. Все, что необходимо сделать - это поменять цвет фона и изменить лэйаут.<br />
<br />
Спасибо за внимание.<br />
<br />
</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-7275945855960005595.post-55606115823283480672011-11-02T10:08:00.001+04:002011-12-13T11:07:32.127+04:00Android. 3D карусель<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/AVvXsEgk3VR0Qzjmj5OIlcy518rextMRNzSmhdFJd2842e4hlIfxnbS65A9WY3y0ioCPs8HupI9ommdQOkHSupd8xvlEtE7SiOVNrSZSvIT0bL-usWPZSOJ9uepVRZkGPxFTkbM9n4DhHsiRV2dP/s1600/carousel.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk3VR0Qzjmj5OIlcy518rextMRNzSmhdFJd2842e4hlIfxnbS65A9WY3y0ioCPs8HupI9ommdQOkHSupd8xvlEtE7SiOVNrSZSvIT0bL-usWPZSOJ9uepVRZkGPxFTkbM9n4DhHsiRV2dP/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/">Source Code</a><br />
<a href="http://horribile.blogspot.com/2011/11/android-3d-carousel.html">English translation</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;">Это моя статья, изначально опубликованная на сайте The Code Project: <a href="http://www.codeproject.com/KB/android/androcarousel.aspx" style="color: #888888; text-decoration: none;">Codeproject</a>.</span><br />
<h2>Введение</h2>Некоторое время назад я искал как можно сделать 3D карусель на под Андроид. Единственное, что я нашел - это была карусель на <strong>UltimateFaves</strong> : [<a href="#ultimatefaves">1</a>]. Но, как оказалось, она ипользует OpenGL. И без исходников. Хотелось бы сделать ее без использования OpenGL. Казалось, что она не должна быть настолько тяжеловесной, для его использования. Продолжая поиски, я наткнулся на <strong>Coverflow Widget</strong> : [<a href="#coverflow">2</a>]. Это виджет использовал лишь 2D библиотеки. В итоге идея оформилась - использовать класс галереи для карусели. Coverflow Widget просто вращает изображения, а я хотел вращать группу. Ok, по крайней мере, будет использоваться простая тригонометрия. Небольшие сложности возники с классом галереи. Если посмотреть статью о Coverflow Widget : [<a href="#coverflow1">3</a>], можно увеидеть, что там описаны несколько проблем, таких как: недоступность членов классов <code>AbsSpinner </code>и <code>AdapterView </code>. Я пошел тем же путем, что и там и переписал некоторые классы. Класс <code>Scroller </code>был заменен классом <code>Rotator </code>, который похож на не него <code>Scroller </code>, но вместо сдвига изображений вращает их группу.<br />
<br />
<h2>Подготовка</h2>Сначала, необходимо определиться с параметрами, которые будут определять поведение карусели. Например, Например минимальное количество элементов в карусели. Она будет выглядеть стрёмно, если в ней будет только 2 элемента, не так ли? Ну, и, чтобы она не сильно грузила девайс определим максимальное количество элементов. Также определим максимальный угол наклона относительно оси Y, набор элементов, текущий элемент, и будет ли отражение. Определим все аттрибуты в файле <em>attrs.xml</em>:<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>Класс CarouselItem </h2>Дабы упростить жизнь, создадим класс <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 />
В нем добавляем позицию в пространстве, индекс элемента и текущий угол поворота. Также сообразим имплементацию <code>Comparable </code>, она будет полезна для порядка отрисовки элементов. <br />
<h2>The Rotator Class </h2>Если посмотреть на исходники класса <code>Scroller </code>class, то можно увидеть два режима: режим скроллинга и режим "броска", в которых вычисляется смещение от исходной точки. Необходимо убрать лишние члены, добавить нужные и заменить вычисления на наши собственные:<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>Отличия CarouselSpinner от AbsSpinner </h2>Во-первых он наследуется от <code>CarouselAdapter </code>, а не от <code>AdapterView</code>. Во-вторых, модифицированный конструктор. В-третьих, измененный метод <code>setSelection(int)</code>. Оставляем только вызов метода <code>setSelectionInt </code>. В-четвертых, добавляем геттеры для недоступных членов. Параметры лэйаута - только <code>WRAP_CONTENT</code>. Основные изменения касаются метода <code>pointToPosition </code>. В классе <code>AbsSpinner</code>, он определяет какой элемент был нажат по экранным координатам. Нам, для этого, необходимо спроецировать элементы на экран:<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>CarouselAdapter vs. AdapterView </h2>Единственные изменения - в методе <code>updateEmptyStatus </code> недоступные члены заменены геттерами. <br />
<h2>Класс Carousel</h2>Класс <code>FlingRunnable </code>заменен классом <code>FlingRotateRunnable </code>, который похож на <code>FlingRunnable </code>, но имеет дело с углами, а не с x-координатой:<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>Также я добавил класс <code>ImageAdapter </code>, который дает возможность делать отражения элементов, как в Coverflow Widget. Также несколько вспомогательных приватных переменных. Конструктор получает список картинок и создает на их основе <code>ImageAdapter </code>. Самое важное в конструкторе - установить поддержку <code>static </code> трансформаций. И разместить картинки по своим местам:<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 />
Далее методы установки элементов. Высоту картинок устанавливаем в 1/3 высоты родительского окна(чтобы влезали при наклоне). Потом надо будет придумать<br />
что-нибудь более оригинальное.<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 />
Посмотрим на метод <code>trackMotionScroll </code>в классе <code>Gallery </code>. Он вызывается в режиме скроллинга и "броска" и выполняет всю необходимую анимацию. Но, двигает элементы только вдоль х-координаты. But it moves images just by x-coordinate. Для вращения в пространстве, его надо переработать. Просто меняем текущий угол и вычисляем позицию в пространстве:<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>Также, по окончании анимации, необходимо расставить элементы по своим местам:<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>Вращение к определенному элементу:<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>Код метода <code>Calculate3DPosition </code>: <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>Удаляем некоторые методы, которые не имеют смысла в нашем случае: <code>offsetChildrenLeftAndRight</code>, <code>detachOffScreenChildren</code>, <code>setSelectionToCenterChild</code>, <code>fillToGalleryLeft</code>, <code>fillToGalleryRight</code>. Самая важная часть находится в методе <code>getChildStaticTransformation </code>. В нем выполняется размещение объектов в пространстве. Он просто берет готовую позицию из элемента, которая была рассчитана в методе <code>Calculate3DPosition: <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>Также, необходимо учитывать, что, если просто вращать элементы, они могут перекрыть друг друга.<br />
Например элемент с z-координатой 100.0 может быть отрисован перед элементом с координатой 50.0. <br />
Чтобы избежать этого, надо переопределить метод <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, здесь еще много надо переработать, отловить баги и оптимизировать, но в первом приближении это работает. Иконки позаимствованы здесь: [<a href="#icons">4</a>]. <br />
<h2>Ресурсы</h2><ol><li><a class="anchor" href="http://ultimatefaves.com/" id="ultimatefaves" name="ultimatefaves" title="ultimatefaves">http://ultimatefaves.com/</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">http://www.inter-fuser.com/2010/02/android-coverflow-widget-v2.html</a> </li>
<li><a class="anchor" href="http://www.inter-fuser.com/2010/01/android-coverflow-widget.html" id="coverflow1" name="coverflow1" title="coverflow1">http://www.inter-fuser.com/2010/01/android-coverflow-widget.html</a> </li>
<li><a class="anchor" href="http://www.iconsmaster.com/Plush-Icons-Set/" id="icons" name="icons" title="icons">http://www.iconsmaster.com/Plush-Icons-Set/</a> </li>
</ol></div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-7275945855960005595.post-35667688492883343402011-10-27T08:39:00.000+04:002011-12-13T11:07:13.195+04:00Android. ImageView с поддержкой SVG<div dir="ltr" style="text-align: left;" trbidi="on"><br />
<a href="http://horribile.blogspot.com/2011/10/android-imageview-with-svg-support.html">English translation</a><br />
<br />
<a href="https://code.google.com/p/image-view-svg/">Source Code</a><br />
<br />
Эта моя стать изначально была опубликована на сайте The Code Project. Оригинал <a href="http://www.codeproject.com/KB/android/AndroidImageViewSVG.aspx">Codeproject</a>.<br />
<br />
<h2>Введение</h2>Как вы знаете, Андроид не поддерживает формат SVG. Однако у него есть масса преимуществ. Во первых - масштабируемость. Отпадает необходимость держать картинки в разных разрешениях. Нет необходимости, к примеру, масштабировать битмэп с потерей качества. SVG может быть отмасштабирована в любое разрешение и качество картинки будет тем же. Во-вторых, SVG - это обычный xml-файл, что в итоге размер файла намного меньше, чем та же картинка в растровом формате. Более того, картинку сожно изменять "на лету". SVG файл можно открыть в обычном текстовом редакторе и помотреть, как картинка формируется. И так далее... Но, так как, к сожалению, Android не поддерживает SVG, то придется немного повозиться с NDK. Хорошая новоость заключается в том, что возни будет немного. У нас имеются готовые библиотеки с открытым исходным кодом для растеризации SVG.<br />
<br />
В интернете достаточно уроков о том, как работать с NDK. Не будем повторяться. Единственно, немного полезных замечаний:<br />
<br />
<ul><li>Во-первых, необходим эклипс. Загрузить можно здесь: [<a href="#eclipse">1</a>]. </li>
<li>Или, как альтернативу, можно использовать [<a href="#motodev">2</a>]. Раньше я предпочитал ее, но в последнее время она без бубна работает не очень...</li>
<li>После установки эклипса, в нем нужно установить CDT плагин. </li>
<li>ADT плагин [<a href="#adt">3</a>]. </li>
<li>После этого, добавляем Eclipse Sequoyah[<a href="#sequoyah">4</a>] плагин, чтобы дебажить c/c++ код.</li>
<li>Для пользователей Windows необходимо установить cygwin[<a href="#cygwin">5</a>] (Добавляем <em>Cygwin/bin</em> в PATH. В эклипсе - cinfigure build path, и устанавливаем build command в что-то типа "bash C:\AndroidNDK\ndk-build"). </li>
<li>Android SDK [<a href="#androidsdk">6</a>]. </li>
<li>Android NDK. Используем CrystaX NDK[<a href="#crystax">7</a>]. (В текущей версии NDK (6) от Google Android NDK libsvg собрать не удалось) </li>
<li>В настройках эклипса указываем пути к NDK и SDKIn</li>
</ul> Сначала используем библиотеку android-libsvg [<a href="#libsvgandroid">8</a>]. Кроме того, она зависит от libsvg, libpng, libjpeg, libexpat и zlib. Ее преимущество в том, что она поддерживает почти все фичи SVG. Создаем папку <em>android-libsvg</em> где-нибудь в файловой системе и в консоли (или cygwin) выполняем “<code><span class="Apple-style-span" style="color: red;">bzr branch lp:libsvg-android</span></code>”. Bazaar загрузит исходники.<br />
<br />
Ok. Теперь создаем новый проект “<code><span class="Apple-style-span" style="color: red;">ImageViewSvg</span></code>”. В контекстном меню проекта выбираем AndroidTools/Add Native support. К проекту добавится папка “<em>jni</em>”. Удаляем из нее все и копируем содержимое папки “<em>jni</em>” из <code><span class="Apple-style-span" style="color: red;">android-libsvg</span> </code>. Обновляем папку <em>jni</em> нашего проекта(F5). Посмотрим на файл <em>Android.mk</em> в папке “<em>jni</em>”. Что означают некоторые переменные:<br />
<br />
<br />
<ul><li><strong>LOCAL_PATH := $(call my-dir) – my-dyr макрос устанавливает LOCAL_PATH переменную, которая указывает где находятся файлы исходников(в текущем каталоге)</strong></li>
<li><strong>include $(CLEAR_VARS) – Очищает вс локальные переменные </strong></li>
<li><strong>LOCAL_MODULE – Имя библиотеки </strong></li>
<li><strong>LOCAL_CFLAGS – Устанавливает флаги компилятора пути к "include" файлам</strong></li>
<li><strong>LIBJPEG_SOURCES, … - Список всех файлов исходников, для каждой библиотеки, которая будет использоваться</strong></li>
<li><strong>LOCAL_LDLIBS – Линки к дополнительным библиотекам</strong></li>
<li><strong>LOCAL_SRC_FILES – Список всех исходников для всех библиотек</strong></li>
<li><strong>BUILD_SHARED_LIBRARY – Линк к mk файлу, который построит "shared" библиотку </strong></li>
</ul><br />
<br />
Более подробно смотрите файл<em>ANDROID-MK.TXT</em> в NDK. <br />
Иногда в WIndows, после рестарта эклипса, он не может запустить ndk-build. Необходимо переустановить "build command" в крнфигурации.<br />
<br />
Далее создаем <code><span class="Apple-style-span" style="color: red;">com.toolkits.libsvgandroid</span> </code>и копируем туда <em>SvgRaster.java</em> из проекта <code><span class="Apple-style-span" style="color: red;">libsvg-android</span> </code>. Все практически готово.<br />
<br />
<br />
Для поддержки классом <code><span class="Apple-style-span" style="color: red;">ImageView</span> </code> SVG формата, необходимо перегрузить в нем некоторые методы. Но еще хотелось бы использовать стандартный атрибут <code><span class="Apple-style-span" style="color: red;">android:src</span> </code>как SVG файл и устанавливать его из стандартной папки“<em>drawable</em>” вместо папки “<em>raw</em>”. Вначале изменим конструктор. Для доступа к <code><span class="Apple-style-span" style="color: red;">android:src</span> </code>атрибуту, добавляем <em>attrs.xml</em> в <em>res/values</em>:<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>Посмотрим на исходники конструктора класса <code><span class="Apple-style-span" style="color: red;">ImageView</span> </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 />
Далее посмотрим на метод <code>setImageBitmap </code>. Он просто вызывает <code>setImageDrawable</code>. Т.о. мы можем использовать его, если у нас будет подходящий битмэп. Правда еще необходимо достать файл из “<em>drawable</em>”. Ну, здесь - как два пальца об асфальт - – достаем ID ресурса из <code>android:src </code>атрибута и читаем его в поток. Далее, при помощи, <code>libandroidsvg </code>парсим SVG: <br />
<br />
В итоге конструктор выглядит так:<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>Другая проблема в том, что в SVG не указан его размер(Не всегда, в некоторых указан желаемый. См. Пы.Сы.). Более того<code><span class="Apple-style-span" style="color: red;">ImageView</span> </code>параметры лэйаута могут быть установлены в <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>, или конкретные размеры. Используем метод <code><span class="Apple-style-span" style="color: red;">onSizeChanged</span> </code>. Единственная проблема - атрибут <code><span class="Apple-style-span" style="color: red;">wrap_content</span> </code>. В этом случае размер будет <code>0</code>. Нуобходимо будет заменить <code><span class="Apple-style-span" style="color: red;">wrap_content</span> </code>на<code><span class="Apple-style-span" style="color: red;">fill_parent</span> </code>на лету. К сожалению, это ничего не даст. Если взглянуть на исходники, то будет видно, что родительски лэйаут вытаскивает параметры прямо из атрибутов и вызывает метод <code><span class="Apple-style-span" style="color: red;">setLayoutParams</span> </code>. Перегрузим его:<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>Также <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 />
И, в конце-концов, настала пора опробовать то, что получилось. Создадим такой лэйаут:<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 />
Запускаемся:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhlNaeOhnxDQCXet0gN2VDiTiaplPGMLU0Su0UegXuSC4REzujX_w9mcJszWcW_iwWFcPsaRbm9XVtc2LxJN1IAlApIekJuxTFkUxfjJO9yqjq00jjakSOG07iUQ_FBV7Dr3C9AGOFKyvU3/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/AVvXsEhlNaeOhnxDQCXet0gN2VDiTiaplPGMLU0Su0UegXuSC4REzujX_w9mcJszWcW_iwWFcPsaRbm9XVtc2LxJN1IAlApIekJuxTFkUxfjJO9yqjq00jjakSOG07iUQ_FBV7Dr3C9AGOFKyvU3/s320/pic1.JPG" width="269" /></a></div><br />
<br />
<br />
<h2>Отладка </h2>Для отладки с/с++ кода следуйте инструкциям Кароса Соунто: [<a href="#debug">9</a>]. Сначала там не все ясно. Несколько советов: <br />
<ul><li>При настройке C++ конфигурации, приложение действительно должно быть <code><span class="Apple-style-span" style="color: red;">app_process</span></code>, не обращаем внимания на жалобы эклипса, что такого файла нет, Он будет создан позже. </li>
<li>Необходимо каждый раз запускать ndk-gdb при запуске приложения. Иногда команда должна быть ndk-gdb –adb=<path-to-sdk>/tools/adb –force.</path-to-sdk></li>
<li>Не забываем ставить "debuggable" в манифесте. </li>
</ul><h2>Другой подход. Anti Grain Geometry. </h2>Это еще одна библиотека для растеризации SVG :[<a href="#antigrain">10</a>]. Единственнаая дополнительная к ней библиотека - <code><span class="Apple-style-span" style="color: red;">libexpat</span></code>. Она уже есть в проекте. В папке <em>jni</em> создаем еще одну:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhr2WT3VJNduWFLhXh9xnO-Gwwl_JLfYYtCsATkeQEJkI9YOy9DpcByMFBFSF9vPl9e52uomOhYhHwS-DdbOipK4d82uKbule6L2sz2ry5PrdxFyPcLmpy0TlYlAPHhsG-TozkUH_Jh1LKf/s1600/pic2.JPG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhr2WT3VJNduWFLhXh9xnO-Gwwl_JLfYYtCsATkeQEJkI9YOy9DpcByMFBFSF9vPl9e52uomOhYhHwS-DdbOipK4d82uKbule6L2sz2ry5PrdxFyPcLmpy0TlYlAPHhsG-TozkUH_Jh1LKf/s1600/pic2.JPG" /></a></div><br />
<br />
Копируем соответствующие файлы из исходников agg в <em>gpc/include/src</em>. Там, в папке <em>examples</em> есть папка<em>svg_viewer</em>. Копируем из нее все, за исключением <em>svg_test</em> в папку<em>aggsvg jni</em> . Единственно, эта библиотека поддерживает только простые SVG. Необходимо будет дописывать парсер. В папке aggsvg-android , создаем файл <em>aggsvgandroid.cpp</em>. Пример парсит SVG из файловой системы. Для строки, добавляем следующий метод в класс <code><span class="Apple-style-span" style="color: red;">parser</span> </code>:<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 />
В конце файла <em>Android.mk</em> , добавляем секцию для построения <br />
еще одной библиотеки. Все довольно просто. ПРосто очищаем переменные после построения первой <br />
библиотеки и настраиваем их для построения второй. <br />
Класс для растеризации с использованием 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 />
В исходниках я добавил возможность протестировать обе библиотеки:<br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaX0DZ046Zr_Y5B1k5gdrbAtIxtkWJxbndK5VenYyXwhekVdZl2SDpmIoV2b7Yso_V7kyv6bULnlgKZWswJxdBzCacHwM-edwFZuHW7k29GIahaQyMzb_kHm2ynzMylHMiwwxGDVYXvoS8/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/AVvXsEhaX0DZ046Zr_Y5B1k5gdrbAtIxtkWJxbndK5VenYyXwhekVdZl2SDpmIoV2b7Yso_V7kyv6bULnlgKZWswJxdBzCacHwM-edwFZuHW7k29GIahaQyMzb_kHm2ynzMylHMiwwxGDVYXvoS8/s320/pic3.JPG" width="256" /></a></div><br />
<br />
<br />
<h2>Заключение </h2>Итак, имеется по меньшей мере два способа использования SVG в Андроиде. Основное преимущество <code>libsvg-android </code> в том, что она готова к использования, но более чем в три раза медленнее AGG, в которой сы вынуждены дорабатывать парсер. Помимо того, в AGG имеется масса возможностей для работы с изображениями. Я показал лишь как можно использовать <code>ImageView </code>в лэйауте, для программного использования необходимо будет еще перегрузить методы такие как <code>setImageResource </code>, к примеру. <br />
<br />
На данный момент все, спасибо за внимание!<br />
<br />
<br />
<h2>Ресурсы </h2><ol><li><a class="anchor" href="http://www.eclipse.org/" id="eclipse" name="eclipse" title="eclipse">http://www.eclipse.org</a> </li>
<li><a class="anchor" href="http://developer.motorola.com/docstools/motodevstudio/" id="motodev" name="motodev" title="motodev">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">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">http://www.eclipse.org/sequoyah/</a> </li>
<li><a class="anchor" href="http://www.cygwin.com/" id="cygwin" name="cygwin" title="cygwin">http://www.cygwin.com/</a> </li>
<li><a class="anchor" href="http://developer.android.com/sdk/index.html" id="androidsdk" name="androidsdk" title="androidsdk">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">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">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">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">http://www.antigrain.com/</a> </li>
</ol><br />
<br />
<br />
P.S. На Code Project вы также можете найти статью об использовании SVG как Drawable:<br />
<a href="http://www.codeproject.com/KB/android/Android_SVG_support.aspx">Drawable with SVG Support</a><br />
<br />
Известные проблемы:<br />
<br />
ibsvg-android не поддерживает представление цвета в виде rgb(255, 255, 255) (Inkscape). (По крайней мере на момент написания статьи.)<br />
Проверьте файл и замените на fill: #ffffff<br />
<br />
Возможно что-то еще. Проверяйте структуру SVG перед использованием.<br />
Неподдреживаемые фичи будут выявляться только при отладке.<br />
<br />
<br />
</div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0tag:blogger.com,1999:blog-7275945855960005595.post-46793595240446624722011-10-25T16:36:00.000+04:002011-12-13T11:09:03.885+04:00Анимированная заставка.<div dir="ltr" style="text-align: left;" trbidi="on"><br />
<h1 class="fn" id="ctl00_TitleArea_ArticleTitle" style="text-align: center;">Анимированный сплеш-скрин для Android - приложения</h1><div>English translation: <a href="http://horribile.blogspot.com/2011/10/animated-splash.html">Animated Splash Screen</a><br />
<br />
Эта статья, написанная мной, изначально была опубликована на сайте Codeproject. Оригинал: <a href="http://www.codeproject.com/KB/android/AndroidSplash.aspx">Codeproject</a>.<br />
<br />
<a href="https://code.google.com/p/android-tips-demo/">Исходники</a><br />
<h2>Введение</h2><div><br />
</div> Каждому хочется, чтобы интерфейс его приложения выглядел привлекательно для пользователя. Множество программ, по крайней мере десктопных приложений (по большей части игры), используют заставки. Это красиво и, более того, пока заставка находится на экране, вы можете инициализировать свое приложение. Существует много руководств, рассказывающих о том, как начать программировать под Андроид, нет смысла их повторять. Вы можете найти их в интернете в огромных количествах. Разберемся с тем, что касается конкретно программирования.<br />
<br />
<br />
<h2>Начало.</h2>Создайте новый проект в эклипсе со следующими настройками:<br />
<br />
<pre class="python" name="code">Project name : AdvancedSplashDemo
Build target: Android 2.1
Application name: Advanced Splash Demo
Package name: Advanced Splash Demo
Create Activity: MainActivity – само приложение
</pre><br />
Что мы имеем изначально - после того, как заставка отработала, она нам больше не нужна. Первая возникающая мысль - использовать активити, которая запускает основную и сама прекращает работу. Создадим лэйаут для заставки - просто LinearLayout с картинкой посередине. Создадим Android XML файл "<i>splash.xml</i>" в папке <i>appfolder/res/layout</i>. Устанавливаем параметры в <span class="Apple-style-span" style="color: red;">wrap_content</span>, для того, чтобы это действительно выглядело как заставка. Выравниваем посередине:<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 />
Значение аттрибута "<span class="Apple-style-span" style="color: red;">layout_gravity</span>" выставлено в "<span class="Apple-style-span" style="color: red;">center</span>" для того, чтобы картинка была посередине экрана. Добавляем картинку в папку <i>appfolder/res/drawable</i> и жмем F5 на проекте.<br />
В исходниках есть файл lnxins.png, и вы можете установить его как источник для ImageView. <br />
<br />
Теперь разберемся с манифестом. Сейчас в нем есть только “<span class="Apple-style-span" style="color: red;">.MainActivity</span>”, указанная как стартовая. Заменим ей категорию на категорию по умолчанию и добавим "<span class="Apple-style-span" style="color: red;">SplashActivity</span>". Она теперь будет являться стартовой. Для этого откроем в манифесте закладку "<span class="Apple-style-span" style="color: red;">Application</span>" и для "<span class="Apple-style-span" style="color: red;">MainActivity</span>" изменим категорию в "<span class="Apple-style-span" style="color: red;">default</span>". Рядом с окном "<span class="Apple-style-span" style="color: red;">Application Nodes</span>" жмем кнопку "<span class="Apple-style-span" style="color: red;">Add...</span>", выбираем из списка "<span class="Apple-style-span" style="color: red;">Activity</span>" и жмем "Ok". Рядом с появившимся пунктом выбираем "<span class="Apple-style-span" style="color: red;">Name*</span>" гиперлинк и вводим имя - <span class="Apple-style-span" style="color: red;">SplashScreen</span>. В исходниках автоматически создастся новый класс. Теперь рядом жмем кнопку "<span class="Apple-style-span" style="color: red;">Add...</span>", чтобы добавить intent filter, action - <span class="Apple-style-span" style="color: red;">MAIN</span> и category - <span class="Apple-style-span" style="color: red;">Launcher</span>. В результате SplashScreen activity будет запущена первой.<br />
<br />
В итоге, манифест должен выглядеть следующим образом:<br />
<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>Немного программирования</h2>Откроем <em>SplashScreen.java</em> файл. На данный момент в нем имеется только один перегруженный метод - <code><span class="Apple-style-span" style="color: red;">onCreate</span></code>. Перегрузим также <code><span class="Apple-style-span" style="color: red;">onTouchEvent</span> </code>для того, чтобы дать пользователю возможность закрыть заставку в любой момент. И не забываем о синхронизации, чтобы приложение не крэшилось в неподходящий момент. В результате имеем:<br />
<br />
<pre class="java" name="code">public class SplashScreen extends Activity {
/**
* Поток для обработки сообщений заставки
*/
private Thread mSplashThread;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Лэйаут заставки
setContentView(R.layout.splash);
final SplashScreen sPlashScreen = this;
// Поток для ожидания событий заставки
mSplashThread = new Thread(){
@Override
public void run(){
try {
synchronized(this){
// Ждем некоторое время, или выход по прикосновению
wait(5000);
}
}
catch(InterruptedException ex){
}
finish();
// Запускаем основную форму
Intent intent = new Intent();
intent.setClass(sPlashScreen, MainActivity.class);
startActivity(intent);
}
};
mSplashThread.start();
}
@Override
public boolean onTouchEvent(MotionEvent evt)
{
if(evt.getAction() == MotionEvent.ACTION_DOWN)
{
synchronized(mSplashThread){
mSplashThread.notifyAll();
}
}
return true;
}
}
</pre><br />
<h2>Немного усовершенствований</h2>Сначала сделаем фон заставки прозрачным. В папке <em>appfolder/res/values</em>, добавим новый Android XML файл <em>styles.xml</em> и определим в нем тему для прозрачности:<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 />
Родительская тема - <code><span class="Apple-style-span" style="color: red;">android:Theme</span>, т.о. мы можем применить ее у к нашей активити</code>. И, как вы видите, имена атрибутов довольно прозрачны, по ним можно понять их предназначение.<br />
Далее, применим тему. В манифесте для SplashScreen activity установим "<span class="Apple-style-span" style="color: red;">theme</span>" атрибут:<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 />
Предположим, что мы разрабатываем игру. Геймеры не очень любят, когда что-то отвлекает их внимание от игрового процесса. Большая их часть предпочитает играть в полноэкранном режиме. Для MainActivity добавим тему для перехода в полноэкранный режим:<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 />
Попробуем запустить то, что у нас получилось. Выглядит лучше. Теперь добавим плавное появление и исчезание. В папке <i>appfolder/res</i> еще одну - "<i>anim</i>" и добавим два Android XML файла – <i>appear.xml</i> и <i>disappear.xml</i>. Они будут определять альфа - анимацию.<br />
<h4><br />
Appear.xml</h4><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><h4><br />
Disappear.xml</h4><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><br />
Они просто изменяют альфа-канал объекта начиная с "<span class="Apple-style-span" style="color: red;">fromAlpa</span>" и заканчивая "<span class="Apple-style-span" style="color: red;">toAlpha</span>" в течение указанного промежутка времени. Добавим новый стиль в <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>В итоге, "<span class="Apple-style-span" style="color: red;">appear</span>" анимация будет выполняться при открытии окна, "<span class="Apple-style-span" style="color: red;">disappear</span>" - при закрытии. Добавим этот стиль к теме прозрачности:<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 />
Зер гут, выглядит уже неплохо. И…<br />
<h2>Не стреляйте в программиста, он рисует как может...</h2>Теперь добавим еще немного анимации. Я художник от слова худо, поэтому использовал Script-Fu скрипт в Гимпе, чтобы сгенерировать хоть какую-то анимацию. Для начала убираем <code>android:src атрибут в</code> <em>splash.xml</em>. Теперь, в папке "<i>drawable</i>" , создадим <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>Здесь просто набор фреймов, и "<span class="Apple-style-span" style="color: red;">oneshot</span>" атрибут говорит о том, что они будут повторяться в цикле. Чтобы запустить данную анимацию изменяем класс заставки - в "<span class="Apple-style-span" style="color: red;">onCreate</span>" методе меняем:<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 />
Мы установили анимацию для заставки, но есть небольшая проблема - мы не можем запустить ее из метода "<span class="Apple-style-span" style="color: red;">onCreate</span>". Анимация должна быть запущена из GUI потока. Для этого используем “<span class="Apple-style-span" style="color: red;">post</span>” метод ImageView класса. Это добавить наш "<span class="Apple-style-span" style="color: red;">Runnable</span>" объект к очереди сообщений и , когда GUI поток будет доступен, запустит его:<br />
<br />
<pre class="java" name="code">splashImageView.post(new Runnable(){
@Override
public void run() {
frameAnimation.start();
}
});
</pre><br />
В итоге: <br />
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEilQkTpkNdiCbALBk_HlIGzxWGDN52vbKmFWJGHZqI2qxvS9CWqR6VJG3kzaMpQzD0udtaT6ilCIi2VDUFGT5thuzcwdr6rGpq81xwzbJ20NK_VCkquAjdJtuaYUmNmjA4Y-7zEI_cuP1p0/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/AVvXsEilQkTpkNdiCbALBk_HlIGzxWGDN52vbKmFWJGHZqI2qxvS9CWqR6VJG3kzaMpQzD0udtaT6ilCIi2VDUFGT5thuzcwdr6rGpq81xwzbJ20NK_VCkquAjdJtuaYUmNmjA4Y-7zEI_cuP1p0/s320/shot.jpg" width="306" /></a></div><br />
Это все. И программируем под Андроид в свое удовольствие :)<br />
<br />
Спасибо за внимание!<br />
<br />
</div></div>Horribilehttp://www.blogger.com/profile/07179002399814427747noreply@blogger.com0