In the "CarouselItem" class there is the "getMatrix" method. But since API level 11 there is such method in View class. Just rename this method to something else in CarouselItem class and in the "pointToPosition" method of the "CarouselSpinner" class.
In the near future I will update the source code.
Thanks
Wednesday, November 30, 2011
ARM released Development Studio CE
ARM has released a new Development Studio Community Edition. The tools work alongside the existing Android Native Development Kit (NDK), and allow low-level access to the processor in ARM devices — crucial to creating high-performance applications on any platform. Ultimately, this means that apps can be written in native code (C or C++)...
It's cool. It's worth a try...
It's cool. It's worth a try...
Friday, November 18, 2011
Formatting EditText input with regular expressions.
Source Code
Русский перевод.
It will be the short article. Now we will format text in EditText with regular expressions.
The simple class will extend Input Filter:
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; } }
The trick is that if the input text does not match the pattern it can match it partially.
If so we will allow the text pasting.
And finally formatting a phone number:
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) {} } );
We have:
Tuesday, November 8, 2011
Two activities on the screen at the same time
Source code
Русский перевод.
Perhaps you've seen that some applications such as winamp, gimp and so on have several separated windows. It was interesting if it's possible to implement such a functionality on Android. Sure it could be done just with layouts, but we are not looking for easy ways.
First, let's define styles:
<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>
The "Theme.Transparent" style is like one we used for the splash screen but lacks "windowIsFloating" item. Due to this the first activity will fill the screen. And for the second activity there is "Theme.Transparent.Floating" style. So this activity won't fill the screen and our touches will be available for the first onу. No. By default activities are modal. And touches won't be available. We must do such workaround:
getWindow().setFlags( WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
Ok, now we see the screen of the device, two activities and first activity is available behind the scene.
But there is one thing to do: communication between these activities. In normal workflow we use
startActivityForResult. But it's not applicable in this case. The simplest way to make them know one of another - to broadcast custom action:
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(); } };
And as a result:
Maybe it will be usefull for something.
Thursday, November 3, 2011
Android. iPhone-style dialog. XML – only.
Source Code
Русский перевод
As the next exercise I will show how to implement the dialog that looks like iPhone alert view. No pictures will be used. We will use only XML.
Really it’s simple. First, let’s define xml-drawable for the button:
<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>
Here we have two layers. The first layer – the rectangle with the gradient. The second layer – the rectangle shifted 20 dip top. This layer should overlap the half of the first one. So, real button should be 40 dip high.
Second, we’ll define the content of the dialog – the header, the text view for a message and the OK button:
<?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>
And at last the background for the dialog will be defined in a code. We will need a drawable with three layers for this – a rectangle shape with white border, a rectangle shape with main color and a rectangle shape that will contain a gloss effect.
To make the white border visible under the first layer we’ll set insets for this layer:
// 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);
The more complicated stuff is in the GlossDrawable class. There we will override onDraw method to calculate where the gloss gradient will be.
The picture describing calculations:
Using the Pythagoras' theorem we have sides of the inscribed triangle:
Then using Heron’s formula we’ll find the area of the inscribed triangle:
And at last the radius:
Now we should draw the circle slightly lower (1/8 of the shape height). The center of the circle will be:
int centerX = (int) shape.getWidth() / 2; int centerY = (int) (-radius + shape.getHeight() / 2);
The rectangle to draw the circle will be:
RectF rectf = new RectF(shape.getWidth() / 2 - radius, shape.getHeight() / 4 - radius * 2, shape.getWidth() / 2 + radius, shape.getHeight() / 4);
Applying the gradient we have:
Use the same technique to make info and confirm dialogs. All you need is to change the background color and the content layout.
Cheerio!
Tuesday, November 1, 2011
Android 3D Carousel
Source Code
Русский перевод.
This my article was originally published on The Code Project web site. You can see it at
Codeproject.
Introduction
For a while, I was looking for a 3D carousel control for Android platform. The only one I found was UltimateFaves at [1]. But as it turned out, it uses OpenGL. And it’s not open source. I thought if it is possible to avoid a use of OpenGL. Continuing my investigations, I stamped on Coverflow Widget at [2]. And it uses standard Android 2D libraries. So the idea was the same – to use Gallery class for the carousel. The Coverflow Widget just rotates images and I wanted to rotate all group of them. Well, at least it implies the use of simple trig methods. More complicated stuff goes with the Gallery class. If you’d look through the article about Coverflow Widget at [3], you’d see a bunch of problems, such as unavailability of default scope variables inAbsSpinner
and AdapterView
classes. So I went the same way and rewrote some classes. And the Scroller
class will be replaced by the Rotator
class which looks like Scroller
but it rotates the group of images.The Preparations
At first, we should decide what parameters will define a behavior of our Carousel. For example, a min quantity of items in the carousel. It will not look nice if it has only one or two items, won’t it? As for performance issue, we have to define max quantity of items. Also, we will need max theta angle for the carousel, what items will be in there, current selected item and if items will be reflected. So let’s define them in attrs.xml file:<?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>
The Carousel Item Class
To simplify some stuff with carousel, I’ve createdCarouselItem
: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); } … }
It incapsulates the position in 3D space, the index of an item and the current angle of an item. Also implementing it as
Comparable
will be helpful when we’ll determine a draw order of the items. The Rotator Class
If you’d look at the source code ofScroller
class, you’ll see two modes: the scroll mode and the fling mode supposed just to calculate current offset from the given start point. We’ll just need to remove extra members, add our own and replace the corresponding calculations: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(); } }
The CarouselSpinner Differences with the AbsSpinner
First, it extendsCarouselAdapter
vs AdapterView
. Those differences I’ll describe later. Second, the modified constructor where the retrieving of AbsSpinner
entries were removed. The third difference is modified setSelection(int)
method. It was just call to setSelectionInt
left. The next change is unavailable variables were replaced with their getters. As for default generated layout parameters, both were set to WRAP_CONTENT
. The main changes concern pointToPosition
method. In AbsSpinner
, it determines if definite item was touched on a screen no matter whether it’s current or not. So, we beed to make the projection from 3D space to screen coordinates: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; }
The CarouselAdapter vs. AdapterView
The only changes are inupdateEmptyStatus
method where unavailable variables were replaced with their getters. The Carousel Class
HereFlingRunnable
class was replaced with FlingRotateRunnable
which is much like FlingRunnable
but makes deal with angle vs. x-coordinate: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); } } }I also added
ImageAdapter
class as it is in Coverflow Widget with a possibility to add a reflection to the images. And some new private
variables were added to support Y-axe angle, reflection and so on. The constructor retrieves list of images, creates ImageAdapter
and sets it. The main thing in the constructor is setting the object to support static
transformations. And to place images into their places:/** * 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(); }
Here are the methods to set up images. The height of an image is set
three times lesser than parent height to make the carousel fit parent
view. It should be reworked later.
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); }
Let’s look at
trackMotionScroll
method in the Gallery
class, it’s called when the widget is being scrolled or flinged and does the necessary stuff for the Gallary animation. But it moves images just by x-coordinate. To make them rotate in 3D space, we must create different functionality. We just change the current angle of an image and calculate it’s position in 3D space: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(); }And after images were flinged or scrolled, we have to place them into the corresponding places:
/** * 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(); } }And to scroll to the definite item:
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); }Here’s the
Calculate3DPosition
method: 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); }Some methods that don’t have a sense with 3D gallery were removed:
offsetChildrenLeftAndRight
, detachOffScreenChildren
, setSelectionToCenterChild
, fillToGalleryLeft
, fillToGalleryRight
. So, the main thing that happens with images is in getChildStaticTransformation
method, where they are transformed in 3D space. It just takes a ready to use position from CarouselImage
class that was calculated by Calculate3DPosition
while flinging/scrolling and moves an image there: 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; }One thing to know is that if you will just rotate images and position them in 3D space, they can overlap each other in the wrong order. For example, an image with 100.0 z-coordinate can be drawn in front of image with 50.0 z-coordinate. To resolve this trouble, we can override
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, it still has a lot to do, like bugs catching and optimization. I didn’t yet test all the functionality, but in the first approximation, it works. Icons were taken from here: [4]. P.S. Fixed bug in
Rotator
class. Jerky "scroll into slots" was made more soft and fluid. Reworked the Rotator
class. It uses only angular acceleration now. Resources
Subscribe to:
Posts (Atom)