В классе "CarouselItem" есть метод "getMatrix". Но начиная с API level 11 такой же метод есть в классе View. Необходимо переименовать этот метод во что-нибудь еще в классе "CarouselItem" и в вызове метода "pointToPosition" в классе "CarouselSpinner".
Как будет время я обновлю код.
среда, 30 ноября 2011 г.
ARM выпустило бесплатную версию Development Studio
ARM выпустило бесплатную версию Development Studio. Студия работает с существующим NDK, предоставляет низкоуровневый доступ на ARM устройствах — для разработки высокопроизводительных приложений. Соответственно, это обозначает, что приложения могут быть написаны на С или С++...
Круто. Стоит попробовать...
Круто. Стоит попробовать...
пятница, 18 ноября 2011 г.
Две активности на экране в одно и то же время
Source code
English translation
Возможно вы видели, что некоторые приложения, такие как winamp, Gimp и так далее имеют несколко раздельных окон. Мне было интересно, возможно ли такое же сделать на Android. Конечно, это можно сделать с использованием лэйаутов и прозрачных тем. Но мы не ищем легких путей.
Сначала определим стили:
"Theme.Transparent" стиль, наподобие того, что бы использован для заставки, но без "windowIsFloating". Благодаря этому первая "activity" заполняет весь экран. Для второй "activity": "Theme.Transparent.Floating". Т.о. вторая не заполнит весь экран и прикосновения к экрану будут доступны первой. Но нет. По умолчанию активности модальны. И прикосновения не будут доступны. Небольшие изменения:
Теперь мы видим экран устройства, две активности и первая из них доступна для прикосновений.
Есть еще одна вещь - взаимодействие между ними. При обычном раскладе мы используем
startActivityForResult. Но в нашем случае это непременимо. Простейший путь - использовать ресивер:
В результате:
Возможно для чего-нибудь будет полезно.
English translation
Возможно вы видели, что некоторые приложения, такие как winamp, Gimp и так далее имеют несколко раздельных окон. Мне было интересно, возможно ли такое же сделать на Android. Конечно, это можно сделать с использованием лэйаутов и прозрачных тем. Но мы не ищем легких путей.
Сначала определим стили:
<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>
"Theme.Transparent" стиль, наподобие того, что бы использован для заставки, но без "windowIsFloating". Благодаря этому первая "activity" заполняет весь экран. Для второй "activity": "Theme.Transparent.Floating". Т.о. вторая не заполнит весь экран и прикосновения к экрану будут доступны первой. Но нет. По умолчанию активности модальны. И прикосновения не будут доступны. Небольшие изменения:
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
Теперь мы видим экран устройства, две активности и первая из них доступна для прикосновений.
Есть еще одна вещь - взаимодействие между ними. При обычном раскладе мы используем
startActivityForResult. Но в нашем случае это непременимо. Простейший путь - использовать ресивер:
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();
}
};
В результате:
Возможно для чего-нибудь будет полезно.
четверг, 3 ноября 2011 г.
"Error" диалог в стиле iPhone. Только XML.
Source Code
English translation
В качестве следующего упражнения, я покажу как сделать диалог, похожий на iPhone AlertView.
Мы не будем использовать для этого какие-либо файлы с картинками. Только XML.
В действительности это не сложно. Для начала создадим define xml-drawable для кнопки:
Здесь у нас два слоя. Первый слой – прямоугольник с градиентом. Второй слой – прямоугольник, сдвинутый вверх на 20dip. Этот слой должен перекрывать половину первого слоя. В итоге, реальная кнопка должна быть высотой в 40dip .
Теперь определим содержимое диалога – заголовок, сообщение и кнопка OK:
И последнее - фон диалога будет определен в коде. На нужен будет "drawable" с тремя слоями – прямоугольник с белой окантовкой, прямоугольник с основным фоновым цветом и прямоугольник с глянцем.
Для того, чтобы белая окантовка была видна, в первом слое устанавливаем отступы:
Более сложные вещи - в классе GlossDrawable. Перегружаем метод onDraw для расчета эффекта глянца.
Картинка, описывающая расчеты:
По теореме Пифагора находим стороны треугольника:
Далее, используя формулу Герона, находим площадь треугольника:
И радиус описанной окружности:
Теперь рисуем окружность чуть ниже (на 1/8 высоты прямоугольника). Центр окружности:
Прямоугольник для отрисовки окружности:
И, применяя градиент, получаем:
Используйте эту же технику для создания "info" и "confirm" диалогов. Все, что необходимо сделать - это поменять цвет фона и изменить лэйаут.
Спасибо за внимание.
English translation
В качестве следующего упражнения, я покажу как сделать диалог, похожий на iPhone AlertView.
Мы не будем использовать для этого какие-либо файлы с картинками. Только XML.
В действительности это не сложно. Для начала создадим define xml-drawable для кнопки:
<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>
Здесь у нас два слоя. Первый слой – прямоугольник с градиентом. Второй слой – прямоугольник, сдвинутый вверх на 20dip. Этот слой должен перекрывать половину первого слоя. В итоге, реальная кнопка должна быть высотой в 40dip .
Теперь определим содержимое диалога – заголовок, сообщение и кнопка OK:
<?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>
И последнее - фон диалога будет определен в коде. На нужен будет "drawable" с тремя слоями – прямоугольник с белой окантовкой, прямоугольник с основным фоновым цветом и прямоугольник с глянцем.
Для того, чтобы белая окантовка была видна, в первом слое устанавливаем отступы:
// 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);
Более сложные вещи - в классе GlossDrawable. Перегружаем метод onDraw для расчета эффекта глянца.
Картинка, описывающая расчеты:
По теореме Пифагора находим стороны треугольника:
Далее, используя формулу Герона, находим площадь треугольника:
И радиус описанной окружности:
Теперь рисуем окружность чуть ниже (на 1/8 высоты прямоугольника). Центр окружности:
int centerX = (int) shape.getWidth() / 2; int centerY = (int) (-radius + shape.getHeight() / 2);
Прямоугольник для отрисовки окружности:
RectF rectf = new RectF(shape.getWidth() / 2 - radius, shape.getHeight() / 4 - radius * 2, shape.getWidth() / 2 + radius, shape.getHeight() / 4);
И, применяя градиент, получаем:
Используйте эту же технику для создания "info" и "confirm" диалогов. Все, что необходимо сделать - это поменять цвет фона и изменить лэйаут.
Спасибо за внимание.
среда, 2 ноября 2011 г.
Android. 3D карусель
Source Code
English translation
Это моя статья, изначально опубликованная на сайте The Code Project: Codeproject.
Введение
Некоторое время назад я искал как можно сделать 3D карусель на под Андроид. Единственное, что я нашел - это была карусель на UltimateFaves : [1]. Но, как оказалось, она ипользует OpenGL. И без исходников. Хотелось бы сделать ее без использования OpenGL. Казалось, что она не должна быть настолько тяжеловесной, для его использования. Продолжая поиски, я наткнулся на Coverflow Widget : [2]. Это виджет использовал лишь 2D библиотеки. В итоге идея оформилась - использовать класс галереи для карусели. Coverflow Widget просто вращает изображения, а я хотел вращать группу. Ok, по крайней мере, будет использоваться простая тригонометрия. Небольшие сложности возники с классом галереи. Если посмотреть статью о Coverflow Widget : [3], можно увеидеть, что там описаны несколько проблем, таких как: недоступность членов классовAbsSpinner и AdapterView . Я пошел тем же путем, что и там и переписал некоторые классы. Класс Scroller был заменен классом Rotator , который похож на не него Scroller , но вместо сдвига изображений вращает их группу.Подготовка
Сначала, необходимо определиться с параметрами, которые будут определять поведение карусели. Например, Например минимальное количество элементов в карусели. Она будет выглядеть стрёмно, если в ней будет только 2 элемента, не так ли? Ну, и, чтобы она не сильно грузила девайс определим максимальное количество элементов. Также определим максимальный угол наклона относительно оси Y, набор элементов, текущий элемент, и будет ли отражение. Определим все аттрибуты в файле attrs.xml:<?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>
Класс CarouselItem
Дабы упростить жизнь, создадим классCarouselItem: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);
}
…
}
В нем добавляем позицию в пространстве, индекс элемента и текущий угол поворота. Также сообразим имплементацию
Comparable , она будет полезна для порядка отрисовки элементов. The Rotator Class
Если посмотреть на исходники классаScroller class, то можно увидеть два режима: режим скроллинга и режим "броска", в которых вычисляется смещение от исходной точки. Необходимо убрать лишние члены, добавить нужные и заменить вычисления на наши собственные: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();
}
}
Отличия CarouselSpinner от AbsSpinner
Во-первых он наследуется отCarouselAdapter , а не от AdapterView. Во-вторых, модифицированный конструктор. В-третьих, измененный метод setSelection(int). Оставляем только вызов метода setSelectionInt . В-четвертых, добавляем геттеры для недоступных членов. Параметры лэйаута - только WRAP_CONTENT. Основные изменения касаются метода pointToPosition . В классе AbsSpinner, он определяет какой элемент был нажат по экранным координатам. Нам, для этого, необходимо спроецировать элементы на экран: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;
}
CarouselAdapter vs. AdapterView
Единственные изменения - в методеupdateEmptyStatus недоступные члены заменены геттерами. Класс Carousel
КлассFlingRunnable заменен классом FlingRotateRunnable , который похож на FlingRunnable , но имеет дело с углами, а не с x-координатой: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);
}
}
}
Также я добавил класс ImageAdapter , который дает возможность делать отражения элементов, как в Coverflow Widget. Также несколько вспомогательных приватных переменных. Конструктор получает список картинок и создает на их основе ImageAdapter . Самое важное в конструкторе - установить поддержку static трансформаций. И разместить картинки по своим местам:/**
* 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();
}
Далее методы установки элементов. Высоту картинок устанавливаем в 1/3 высоты родительского окна(чтобы влезали при наклоне). Потом надо будет придумать
что-нибудь более оригинальное.
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);
}
Посмотрим на метод
trackMotionScroll в классе Gallery . Он вызывается в режиме скроллинга и "броска" и выполняет всю необходимую анимацию. Но, двигает элементы только вдоль х-координаты. But it moves images just by x-coordinate. Для вращения в пространстве, его надо переработать. Просто меняем текущий угол и вычисляем позицию в пространстве: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();
}
Также, по окончании анимации, необходимо расставить элементы по своим местам:/**
* 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();
}
}
Вращение к определенному элементу: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);
}
Код метода Calculate3DPosition : 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);
}
Удаляем некоторые методы, которые не имеют смысла в нашем случае: offsetChildrenLeftAndRight, detachOffScreenChildren, setSelectionToCenterChild, fillToGalleryLeft, fillToGalleryRight. Самая важная часть находится в методе getChildStaticTransformation . В нем выполняется размещение объектов в пространстве. Он просто берет готовую позицию из элемента, которая была рассчитана в методе Calculate3DPosition:
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;
}
Также, необходимо учитывать, что, если просто вращать элементы, они могут перекрыть друг друга.
Например элемент с z-координатой 100.0 может быть отрисован перед элементом с координатой 50.0.
Чтобы избежать этого, надо переопределить метод getChildDrawingOrder:
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;
}
Ok, здесь еще много надо переработать, отловить баги и оптимизировать, но в первом приближении это работает. Иконки позаимствованы здесь: [4].
Ресурсы
Подписаться на:
Комментарии (Atom)


