/* * Copyright (C) 2012 www.amsoft.cn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.andbase.view.carousel; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import android.content.Context; import android.graphics.Camera; import android.graphics.Matrix; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.ContextMenu.ContextMenuInfo; import android.view.GestureDetector; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Transformation; // TODO: Auto-generated Javadoc /** * © 2012 amsoft.cn * 名称:CarouselImageView.java * 描述:图片适配 * * @author 还如一梦中 * @version v1.0 * @date:2013-11-28 上午11:15:11 */ public class CarouselImageView extends CarouselSpinner implements GestureDetector.OnGestureListener { // Static private members /** Tag for a class logging. */ private static final String TAG = CarouselImageView.class.getSimpleName(); /** If logging should be inside class. */ private static final boolean localLOGV = false; /** Default min quantity of images. */ private static final int MIN_QUANTITY = 3; /** Default max quantity of images. */ private static final int MAX_QUANTITY = 12; /** Max theta. */ private static final float MAX_THETA = 15.0f; /** * Duration in milliseconds from the start of a scroll during which we're * unsure whether the user is scrolling or flinging. */ private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250; // Private members /** The info for adapter context menu. */ private AdapterContextMenuInfo mContextMenuInfo; /** * How long the transition animation should run when a child view changes * position, measured in milliseconds. */ private int mAnimationDuration = 900; /** Camera to make 3D rotation. */ private Camera mCamera = new Camera(); /** * Sets mSuppressSelectionChanged = false. This is used to set it to false * in the future. It will also trigger a selection changed. */ private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() { public void run() { mSuppressSelectionChanged = false; selectionChanged(); } }; /** * The position of the item that received the user's down touch. */ private int mDownTouchPosition; /** * The view of the item that received the user's down touch. */ private View mDownTouchView; /** * Executes the delta rotations from a fling or scroll movement. */ private FlingRotateRunnable mFlingRunnable = new FlingRotateRunnable(); /** * Helper for detecting touch gestures. */ private GestureDetector mGestureDetector; /** Gravity for the widget. */ private int mGravity; /** * If true, this onScroll is the first for this user's drag (remember, a * drag sends many onScrolls). */ private boolean mIsFirstScroll; /** Set max qantity of images. */ private int mMaxQuantity = MAX_QUANTITY; /** Set min quantity of images. */ private int mMinQuantity = MIN_QUANTITY; /** * If true, we have received the "invoke" (center or enter buttons) key * down. This is checked before we action on the "invoke" key up, and is * subsequently cleared. */ private boolean mReceivedInvokeKeyDown; /** * The currently selected item's child. */ private View mSelectedChild; /** * Whether to continuously callback on the item selected listener during a * fling. */ private boolean mShouldCallbackDuringFling = true; /** * Whether to callback when an item that is not selected is clicked. */ private boolean mShouldCallbackOnUnselectedItemClick = true; /** * When fling runnable runs, it resets this to false. Any method along the * path until the end of its run() can set this to true to abort any * remaining fling. For example, if we've reached either the leftmost or * rightmost item, we will set this to true. */ private boolean mShouldStopFling; /** * If true, do not callback to item selected listener. */ private boolean mSuppressSelectionChanged; /** The axe angle. */ private float mTheta = (float)(15.0f*(Math.PI/180.0)); /** If items should be reflected. */ private boolean mUseReflection; // Constructors /** * Instantiates a new carousel image view. * * @param context the context */ public CarouselImageView(Context context) { this(context, null); } /** * Instantiates a new carousel image view. * * @param context the context * @param attrs the attrs */ public CarouselImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * Instantiates a new carousel image view. * * @param context the context * @param attrs the attrs * @param defStyle the def style */ public CarouselImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // It's needed to make items with greater value of // z coordinate to be behind items with lesser z-coordinate setChildrenDrawingOrderEnabled(true); // Making user gestures available mGestureDetector = new GestureDetector(this.getContext(), this); mGestureDetector.setIsLongpressEnabled(true); // It's needed to apply 3D transforms to items // before they are drawn setStaticTransformationsEnabled(true); // Retrieve settings mAnimationDuration = 400; mUseReflection = false; int selectedItem = 0; // next time we go through layout with this value setNextSelectedPositionInt(selectedItem); } // View overrides // These are for use with horizontal scrollbar /** * Compute the horizontal extent of the horizontal scrollbar's thumb * within the horizontal range. This value is used to compute * the length of the thumb within the scrollbar's track. * * @return the int */ @Override protected int computeHorizontalScrollExtent() { // Only 1 item is considered to be selected return 1; } /** * Compute the horizontal offset of the horizontal scrollbar's * thumb within the horizontal range. This value is used to compute * the position of the thumb within the scrollbar's track. * * @return the int */ @Override protected int computeHorizontalScrollOffset() { // Current scroll position is the same as the selected position return mSelectedPosition; } /** * Compute the horizontal range that the horizontal scrollbar represents. * * @return the int */ @Override protected int computeHorizontalScrollRange() { // Scroll range is the same as the item count return mItemCount; } /** * Implemented to handle touch screen motion events. * * @param event the event * @return true, if successful */ @Override public boolean onTouchEvent(MotionEvent event) { // Give everything to the gesture detector boolean retValue = mGestureDetector.onTouchEvent(event); int action = event.getAction(); if (action == MotionEvent.ACTION_UP) { // Helper method for lifted finger onUp(); } else if (action == MotionEvent.ACTION_CANCEL) { onCancel(); } return retValue; } /** * Extra information about the item for which the context menu should be shown. * * @return the context menu info */ @Override protected ContextMenuInfo getContextMenuInfo() { return mContextMenuInfo; } /** * Bring up the context menu for this view. * * @return true, if successful */ @Override public boolean showContextMenu() { if (isPressed() && mSelectedPosition >= 0) { int index = mSelectedPosition - mFirstPosition; View v = getChildAt(index); return dispatchLongPress(v, mSelectedPosition, mSelectedRowId); } return false; } /** * Handles left, right, and clicking. * * @param keyCode the key code * @param event the event * @return true, if successful * @see android.view.View#onKeyDown */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: ////if (movePrevious()) { playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); ////} return true; case KeyEvent.KEYCODE_DPAD_RIGHT: /////if (moveNext()) { playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); ////} return true; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: mReceivedInvokeKeyDown = true; // fallthrough to default handling } return super.onKeyDown(keyCode, event); } /** * 描述:TODO. * * @version v1.0 * @param keyCode the key code * @param event the event * @return true, if successful * @see android.view.View#onKeyUp(int, android.view.KeyEvent) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: { if (mReceivedInvokeKeyDown) { if (mItemCount > 0) { dispatchPress(mSelectedChild); postDelayed(new Runnable() { public void run() { dispatchUnpress(); } }, ViewConfiguration.getPressedStateDuration()); int selectedIndex = mSelectedPosition - mFirstPosition; performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter .getItemId(mSelectedPosition)); } } // Clear the flag mReceivedInvokeKeyDown = false; return true; } } return super.onKeyUp(keyCode, event); } /** * 描述:TODO. * * @version v1.0 * @param gainFocus the gain focus * @param direction the direction * @param previouslyFocusedRect the previously focused rect * @see android.view.View#onFocusChanged(boolean, int, android.graphics.Rect) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); /* * The gallery shows focus by focusing the selected item. So, give * focus to our selected item instead. We steal keys from our * selected item elsewhere. */ if (gainFocus && mSelectedChild != null) { mSelectedChild.requestFocus(direction); } } // ViewGroup overrides /** * 描述:TODO. * * @version v1.0 * @param p the p * @return true, if successful * @see android.view.ViewGroup#checkLayoutParams(android.view.ViewGroup.LayoutParams) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } /** * 描述:TODO. * * @version v1.0 * @param p the p * @return the view group. layout params * @see android.view.ViewGroup#generateLayoutParams(android.view.ViewGroup.LayoutParams) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } /** * 描述:TODO. * * @version v1.0 * @param attrs the attrs * @return the view group. layout params * @see android.view.ViewGroup#generateLayoutParams(android.util.AttributeSet) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } /** * 描述:TODO. * * @version v1.0 * @param selected the selected * @see android.view.ViewGroup#dispatchSetSelected(boolean) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override public void dispatchSetSelected(boolean selected) { /* * We don't want to pass the selected state given from its parent to its * children since this widget itself has a selected state to give to its * children. */ } /** * 描述:TODO. * * @version v1.0 * @param pressed the pressed * @see android.view.ViewGroup#dispatchSetPressed(boolean) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override protected void dispatchSetPressed(boolean pressed) { // Show the pressed state on the selected child if (mSelectedChild != null) { mSelectedChild.setPressed(pressed); } } /** * 描述:TODO. * * @version v1.0 * @param originalView the original view * @return true, if successful * @see android.view.ViewGroup#showContextMenuForChild(android.view.View) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override public boolean showContextMenuForChild(View originalView) { final int longPressPosition = getPositionForView(originalView); if (longPressPosition < 0) { return false; } final long longPressId = mAdapter.getItemId(longPressPosition); return dispatchLongPress(originalView, longPressPosition, longPressId); } /** * 描述:TODO. * * @version v1.0 * @param event the event * @return true, if successful * @see android.view.ViewGroup#dispatchKeyEvent(android.view.KeyEvent) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override public boolean dispatchKeyEvent(KeyEvent event) { // Gallery steals all key events return event.dispatch(this, null, null); } /** * Index of the child to draw for this iteration. * * @param childCount the child count * @param i the i * @return the child drawing order */ @Override protected int getChildDrawingOrder(int childCount, int i) { // Sort Carousel items by z coordinate in reverse order ArrayList<CarouselItemImage> sl = new ArrayList<CarouselItemImage>(); for(int j = 0; j < childCount; j++) { CarouselItemImage view = (CarouselItemImage)getAdapter().getView(j,null, null); if(i == 0) view.setDrawn(false); sl.add((CarouselItemImage)getAdapter().getView(j,null, null)); } Collections.sort(sl); // Get first undrawn item in array and get result index int idx = 0; for(CarouselItemImage civ : sl) { if(!civ.isDrawn()) { civ.setDrawn(true); idx = civ.getIndex(); break; } } return idx; } /** * Transform an item depending on it's coordinates. * * @param child the child * @param transformation the transformation * @return the child static transformation */ @Override protected boolean getChildStaticTransformation(View child, Transformation transformation) { transformation.clear(); transformation.setTransformationType(Transformation.TYPE_MATRIX); // Center of the view float centerX = (float)getWidth()/2, centerY = (float)getHeight()/2; // Save camera mCamera.save(); // Translate the item to it's coordinates final Matrix matrix = transformation.getMatrix(); mCamera.translate(((CarouselItemImage)child).getItemX(), ((CarouselItemImage)child).getItemY(), ((CarouselItemImage)child).getItemZ()); // Align the item mCamera.getMatrix(matrix); matrix.preTranslate(-centerX, -centerY); matrix.postTranslate(centerX, centerY); float[] values = new float[9]; matrix.getValues(values); // Restore camera mCamera.restore(); Matrix mm = new Matrix(); mm.setValues(values); ((CarouselItemImage)child).setCIMatrix(mm); //http://code.google.com/p/android/issues/detail?id=35178 child.invalidate(); return true; } // CarouselAdapter overrides /** * Setting up images. * * @param delta the delta * @param animate the animate */ void layout(int delta, boolean animate){ if (mDataChanged) { handleDataChanged(); } // Handle an empty gallery by removing all views. if (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(); } /** * Setting up images after layout changed. * * @param changed the changed * @param l the l * @param t the t * @param r the r * @param b the b */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); /* * Remember that we are in layout to prevent more layout request from * being generated. */ mInLayout = true; layout(0, false); mInLayout = false; } /** * 描述:TODO. * * @version v1.0 * @see com.ab.view.carousel.CarouselAdapter#selectionChanged() * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override void selectionChanged() { if (!mSuppressSelectionChanged) { super.selectionChanged(); } } /** * 描述:TODO. * * @version v1.0 * @param position the new selected position int * @see com.ab.view.carousel.CarouselAdapter#setSelectedPositionInt(int) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ @Override void setSelectedPositionInt(int position) { super.setSelectedPositionInt(position); super.setNextSelectedPositionInt(position); // Updates any metadata we keep about the selected item. updateSelectedItemMetadata(); } // Rotation class for the Carousel /** * The Class FlingRotateRunnable. */ 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()); } /** * Start common. */ private void startCommon() { // Remove any pending flings removeCallbacks(this); } /** * Start using velocity. * * @param initialVelocity the initial velocity */ public void startUsingVelocity(float initialVelocity) { if (initialVelocity == 0) return; startCommon(); mLastFlingAngle = 0.0f; mRotator.fling(initialVelocity); post(this); } /** * Start using distance. * * @param deltaAngle the delta angle */ public void startUsingDistance(float deltaAngle) { if (deltaAngle == 0) return; startCommon(); mLastFlingAngle = 0; synchronized(this) { mRotator.startRotate(0.0f, -deltaAngle, mAnimationDuration); } post(this); } /** * Stop. * * @param scrollIntoSlots the scroll into slots */ public void stop(boolean scrollIntoSlots) { removeCallbacks(this); endFling(scrollIntoSlots); } /** * End fling. * * @param scrollIntoSlots the scroll into slots */ 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(); } /** * 描述:TODO. * * @version v1.0 * @see java.lang.Runnable#run() * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ public void run() { if (CarouselImageView.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; //////// Shoud be reworked trackMotionScroll(delta); if (more && !mShouldStopFling) { mLastFlingAngle = angle; post(this); } else { mLastFlingAngle = 0.0f; endFling(true); } } } // OnGestureListener implementation /** * 描述:TODO. * * @version v1.0 * @param e the e * @return true, if successful * @see android.view.GestureDetector.OnGestureListener#onDown(android.view.MotionEvent) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ public boolean onDown(MotionEvent e) { // Kill any existing fling/scroll mFlingRunnable.stop(false); ///// Don't know yet what for it is // Get the item's view that was touched mDownTouchPosition = pointToPositionImage((int) e.getX(), (int) e.getY()); if (mDownTouchPosition >= 0) { mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition); mDownTouchView.setPressed(true); } // Reset the multiple-scroll tracking state mIsFirstScroll = true; // Must return true to get matching events for this down event. return true; } /** * 描述:TODO. * * @version v1.0 * @param e1 the e1 * @param e2 the e2 * @param velocityX the velocity x * @param velocityY the velocity y * @return true, if successful * @see android.view.GestureDetector.OnGestureListener#onFling(android.view.MotionEvent, android.view.MotionEvent, float, float) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (!mShouldCallbackDuringFling) { // We want to suppress selection changes // Remove any future code to set mSuppressSelectionChanged = false removeCallbacks(mDisableSuppressSelectionChangedRunnable); // This will get reset once we scroll into slots if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true; } // Fling the gallery! //mFlingRunnable.startUsingVelocity((int) -velocityX); mFlingRunnable.startUsingVelocity((int) velocityX); return true; } /** * 描述:TODO. * * @version v1.0 * @param e the e * @see android.view.GestureDetector.OnGestureListener#onLongPress(android.view.MotionEvent) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ public void onLongPress(MotionEvent e) { if (mDownTouchPosition < 0) { return; } performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); long id = getItemIdAtPosition(mDownTouchPosition); dispatchLongPress(mDownTouchView, mDownTouchPosition, id); } /** * 描述:TODO. * * @version v1.0 * @param e1 the e1 * @param e2 the e2 * @param distanceX the distance x * @param distanceY the distance y * @return true, if successful * @see android.view.GestureDetector.OnGestureListener#onScroll(android.view.MotionEvent, android.view.MotionEvent, float, float) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX())); /* * Now's a good time to tell our parent to stop intercepting our events! * The user has moved more than the slop amount, since GestureDetector * ensures this before calling this method. Also, if a parent is more * interested in this touch's events than we are, it would have * intercepted them by now (for example, we can assume when a Gallery is * in the ListView, a vertical scroll would not end up in this method * since a ListView would have intercepted it by now). */ getParent().requestDisallowInterceptTouchEvent(true); // As the user scrolls, we want to callback selection changes so related- // info on the screen is up-to-date with the gallery's selection if (!mShouldCallbackDuringFling) { if (mIsFirstScroll) { /* * We're not notifying the client of selection changes during * the fling, and this scroll could possibly be a fling. Don't * do selection changes until we're sure it is not a fling. */ if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true; postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT); } } else { if (mSuppressSelectionChanged) mSuppressSelectionChanged = false; } // Track the motion trackMotionScroll(/* -1 * */ (int) distanceX); mIsFirstScroll = false; return true; } /** * 描述:TODO. * * @version v1.0 * @param e the e * @return true, if successful * @see android.view.GestureDetector.OnGestureListener#onSingleTapUp(android.view.MotionEvent) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ public boolean onSingleTapUp(MotionEvent e) { if (mDownTouchPosition >= 0) { // Pass the click so the client knows, if it wants to. if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) { performItemClick(mDownTouchView, mDownTouchPosition, mAdapter .getItemId(mDownTouchPosition)); } return true; } return false; } ///// Unused gestures /** * 描述:TODO. * * @version v1.0 * @param e the e * @see android.view.GestureDetector.OnGestureListener#onShowPress(android.view.MotionEvent) * @author: amsoft.cn * @date:2013-11-28 上午11:14:34 */ public void onShowPress(MotionEvent e) { } /** * Calculate3 d position. * * @param child the child * @param diameter the diameter * @param angleOffset the angle offset */ private void Calculate3DPosition(CarouselItemImage child, int diameter, float angleOffset){ angleOffset = angleOffset * (float)(Math.PI/180.0f); float x = - (float)(diameter/2 * android.util.FloatMath.sin(angleOffset)) + diameter/2 - child.getWidth()/2; float z = diameter/2 * (1.0f - (float)android.util.FloatMath.cos(angleOffset)); float y = - getHeight()/2 + (float) (z * android.util.FloatMath.sin(mTheta)); child.setItemX(x); child.setItemZ(z); child.setItemY(y); } /** * Figure out vertical placement based on mGravity. * * @param child Child to place * @param duringLayout the during layout * @return Where the top of the child should be */ private int calculateTop(View child, boolean duringLayout) { int myHeight = duringLayout ? getMeasuredHeight() : getHeight(); int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight(); int childTop = 0; switch (mGravity) { case Gravity.TOP: childTop = mSpinnerPadding.top; break; case Gravity.CENTER_VERTICAL: int availableSpace = myHeight - mSpinnerPadding.bottom - mSpinnerPadding.top - childHeight; childTop = mSpinnerPadding.top + (availableSpace / 2); break; case Gravity.BOTTOM: childTop = myHeight - mSpinnerPadding.bottom - childHeight; break; } return childTop; } /** * Dispatch long press. * * @param view the view * @param position the position * @param id the id * @return true, if successful */ private boolean dispatchLongPress(View view, int position, long id) { boolean handled = false; if (mOnItemLongClickListener != null) { handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView, mDownTouchPosition, id); } if (!handled) { mContextMenuInfo = new AdapterContextMenuInfo(view, position, id); handled = super.showContextMenuForChild(this); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; } /** * Dispatch press. * * @param child the child */ private void dispatchPress(View child) { if (child != null) { child.setPressed(true); } setPressed(true); } /** * Dispatch unpress. */ private void dispatchUnpress() { for (int i = getChildCount() - 1; i >= 0; i--) { getChildAt(i).setPressed(false); } setPressed(false); } /** * Gets the center of gallery. * * @return The center of this Gallery. */ private int getCenterOfGallery() { return (getWidth() - CarouselImageView.this.getPaddingLeft() - CarouselImageView.this.getPaddingRight()) / 2 + CarouselImageView.this.getPaddingLeft(); } /** * Gets the center of view. * * @param view the view * @return The center of the given view. */ private static int getCenterOfView(View view) { return view.getLeft() + view.getWidth() / 2; } /** * Gets the limited motion scroll amount. * * @param motionToLeft the motion to left * @param deltaX the delta x * @return the limited motion scroll amount */ float getLimitedMotionScrollAmount(boolean motionToLeft, float deltaX) { int extremeItemPosition = motionToLeft ? CarouselImageView.this.getCount() - 1 : 0; View extremeChild = getChildAt(extremeItemPosition - CarouselImageView.this.getFirstVisiblePosition()); if (extremeChild == null) { return deltaX; } int extremeChildCenter = getCenterOfView(extremeChild); int galleryCenter = getCenterOfGallery(); if (motionToLeft) { if (extremeChildCenter <= galleryCenter) { // The extreme child is past his boundary point! return 0; } } else { if (extremeChildCenter >= galleryCenter) { // The extreme child is past his boundary point! return 0; } } int centerDifference = galleryCenter - extremeChildCenter; return motionToLeft ? Math.max(centerDifference, deltaX) : Math.min(centerDifference, deltaX); } /** * Gets the limited motion scroll amount. * * @param motionToLeft the motion to left * @param deltaX the delta x * @return the limited motion scroll amount */ int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) { int extremeItemPosition = motionToLeft ? mItemCount - 1 : 0; View extremeChild = getChildAt(extremeItemPosition - mFirstPosition); if (extremeChild == null) { return deltaX; } int extremeChildCenter = getCenterOfView(extremeChild); int galleryCenter = getCenterOfGallery(); if (motionToLeft) { if (extremeChildCenter <= galleryCenter) { // The extreme child is past his boundary point! return 0; } } else { if (extremeChildCenter >= galleryCenter) { // The extreme child is past his boundary point! return 0; } } int centerDifference = galleryCenter - extremeChildCenter; return motionToLeft ? Math.max(centerDifference, deltaX) : Math.min(centerDifference, deltaX); } /** * Make and add view. * * @param position the position * @param angleOffset the angle offset */ private void makeAndAddView(int position, float angleOffset) { CarouselItemImage child; if (!mDataChanged) { child = (CarouselItemImage)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 = (CarouselItemImage)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 = (CarouselItemImage)mAdapter.getView(position, null, this); // Position the view setUpChild(child, child.getIndex(), angleOffset); } /** * On cancel. */ void onCancel(){ onUp(); } /** * Called when rotation is finished. */ private void onFinishedMovement() { if (mSuppressSelectionChanged) { mSuppressSelectionChanged = false; // We haven't been callbacking during the fling, so do it now super.selectionChanged(); } checkSelectionChanged(); invalidate(); } /** * On up. */ void onUp(){ if (mFlingRunnable.mRotator.isFinished()) { scrollIntoSlots(); } dispatchUnpress(); } /** * 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<CarouselItemImage> arr = new ArrayList<CarouselItemImage>(); for(int i = 0; i < getAdapter().getCount(); i++) arr.add(((CarouselItemImage)getAdapter().getView(i, null, null))); Collections.sort(arr, new Comparator<CarouselItemImage>(){ public int compare(CarouselItemImage c1, CarouselItemImage 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(); } } /** * Scroll to child. * * @param i the i */ void scrollToChild(int i){ CarouselItemImage view = (CarouselItemImage)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); } /** * Whether or not to callback on any {@link #getOnItemSelectedListener()} * while the items are being flinged. If false, only the final selected item * will cause the callback. If true, all items between the first and the * final will cause callbacks. * * @param shouldCallback Whether or not to callback on the listener while * the items are being flinged. */ public void setCallbackDuringFling(boolean shouldCallback) { mShouldCallbackDuringFling = shouldCallback; } /** * Whether or not to callback when an item that is not selected is clicked. * If false, the item will become selected (and re-centered). If true, the * * @param shouldCallback Whether or not to callback on the listener when a * item that is not selected is clicked. * {@link #getOnItemClickListener()} will get the callback. * @hide */ public void setCallbackOnUnselectedItemClick(boolean shouldCallback) { mShouldCallbackOnUnselectedItemClick = shouldCallback; } /** * Sets how long the transition animation should run when a child view * changes position. Only relevant if animation is turned on. * * @param animationDurationMillis The duration of the transition, in * milliseconds. * * @attr ref android.R.styleable#Gallery_animationDuration */ public void setAnimationDuration(int animationDurationMillis) { mAnimationDuration = animationDurationMillis; } /** * Sets the gravity. * * @param gravity the new gravity */ public void setGravity(int gravity){ if (mGravity != gravity) { mGravity = gravity; requestLayout(); } } /** * Helper for makeAndAddView to set the position of a view and fill out its * layout paramters. * * @param child The view to position * @param index the index * @param angleOffset the angle offset */ private void setUpChild(CarouselItemImage 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); } /** * Tracks a motion scroll. In reality, this is used to do just about any * movement to items (touch scroll, arrow-key scroll, set an item as selected). * * @param deltaAngle Change in X from the previous event. */ void trackMotionScroll(float deltaAngle) { if (getChildCount() == 0) { return; } for(int i = 0; i < getAdapter().getCount(); i++){ CarouselItemImage child = (CarouselItemImage)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(); } /** * Update selected item metadata. */ private void updateSelectedItemMetadata() { View oldSelectedChild = mSelectedChild; View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition); if (child == null) { return; } child.setSelected(true); child.setFocusable(true); if (hasFocus()) { child.requestFocus(); } // We unfocus the old child down here so the above hasFocus check // returns true if (oldSelectedChild != null) { // Make sure its drawable state doesn't contain 'selected' oldSelectedChild.setSelected(false); // Make sure it is not focusable anymore, since otherwise arrow keys // can make this one be focused oldSelectedChild.setFocusable(false); } } }