In my other topics about animation methods and problems with gallery views I made mention of writing my own gallery view. I'm pretty new to Android but I found some helpful posts and helpful people on this group so I am posting up my code for the benefit of anyone who is curious. Feel free to critique, comment or offer suggestions.
For a limited time I will have the full project on my Zumo drive at http://www.zumodrive.com/share/5SBbYzk0Mj (The original topics were: " Soft keyboard “del” key fails in edittext on Gallery widget " and "What is best method for an animation worker thread?" if you want to reference them) Class "FlingGallery" --------------------------------------------------------------------------------------------------------- package com.droidful.flinggallery; import android.content.Context; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.animation.Transformation; import android.widget.Adapter; import android.widget.FrameLayout; import android.widget.LinearLayout; public class FlingGallery extends FrameLayout { // Constants private final int swipe_min_distance = 120; private final int swipe_max_off_path = 250; private final int swipe_threshold_veloicty = 400; // Properties private int mViewPaddingWidth = 0; private int mAnimationDuration = 400; private float mSnapBorderRatio = 0.4f; private boolean mIsFastFlingLinear = true; private boolean mIsGalleryCircular = true; // Members private int mDisplayWidth = 0; private int mGalleryWidth = 0; private boolean mIsTouched = false; private boolean mIsDragging = false; private float mCurrentOffset = 0.0f; private int mFlingDirection = 0; private float mFlingVelocity = 0.0f; private int mCurrentPosition = 0; private int mCurrentViewNumber = 0; private Context mContext; private Adapter mAdapter; private FlingGalleryView[] mViews; private FlingGalleryAnimation mAnimation; private GestureDetector mGestureDetector; private Interpolator mDecelerateInterpolater; private Interpolator mLinearInterpolater; public FlingGallery(Context context) { super(context); mContext = context; mAdapter = null; WindowManager windowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); mDisplayWidth = windowManager.getDefaultDisplay().getWidth(); mViews = new FlingGalleryView[3]; mViews[0] = new FlingGalleryView(0, this); mViews[1] = new FlingGalleryView(1, this); mViews[2] = new FlingGalleryView(2, this); mAnimation = new FlingGalleryAnimation(); mGestureDetector = new GestureDetector(new FlingGestureDetector()); mDecelerateInterpolater = AnimationUtils.loadInterpolator(mContext, android.R.anim.decelerate_interpolator); mLinearInterpolater = AnimationUtils.loadInterpolator(mContext, android.R.anim.linear_interpolator); } public void setPaddingWidth(int viewPaddingWidth) { mViewPaddingWidth = viewPaddingWidth; } public void setAnimationDuration(int animationDuration) { mAnimationDuration = animationDuration; } public void setSnapBorderRatio(float snapBorderRatio) { mSnapBorderRatio = snapBorderRatio; } public void setIsFastFlingLinear(boolean isFastFlingLinear) { mIsFastFlingLinear = isFastFlingLinear; } public void setIsGalleryCircular(boolean isGalleryCircular) { if (mIsGalleryCircular != isGalleryCircular) { mIsGalleryCircular = isGalleryCircular; if (mCurrentPosition == getFirstPosition()) { // We need to reload the view immediately to the left to change it to circular view or blank mViews[getPrevViewNumber(mCurrentViewNumber)].recycleView(getPrevPosition(mCurrentPosition)); } if (mCurrentPosition == getLastPosition()) { // We need to reload the view immediately to the right to change it to circular view or blank mViews[getNextViewNumber(mCurrentViewNumber)].recycleView(getNextPosition(mCurrentPosition)); } } } public int getGalleryCount() { return (mAdapter == null) ? 0 : mAdapter.getCount(); } public int getFirstPosition() { return 0; } public int getLastPosition() { return (getGalleryCount() == 0) ? 0 : getGalleryCount() - 1; } private int getPrevPosition(int relativePosition) { int prevPosition = relativePosition - 1; if (prevPosition < getFirstPosition()) { prevPosition = getFirstPosition() - 1; if (mIsGalleryCircular == true) { prevPosition = getLastPosition(); } } return prevPosition; } private int getNextPosition(int relativePosition) { int nextPosition = relativePosition + 1; if (nextPosition > getLastPosition()) { nextPosition = getLastPosition() + 1; if (mIsGalleryCircular == true) { nextPosition = getFirstPosition(); } } return nextPosition; } private int getPrevViewNumber(int relativeViewNumber) { return (relativeViewNumber == 0) ? 2 : relativeViewNumber - 1; } private int getNextViewNumber(int relativeViewNumber) { return (relativeViewNumber == 2) ? 0 : relativeViewNumber + 1; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); // Calculate our view width mGalleryWidth = right - left; if (changed == true) { // Position views at correct starting offsets mViews[0].setOffset(0, 0, mCurrentViewNumber); mViews[1].setOffset(0, 0, mCurrentViewNumber); mViews[2].setOffset(0, 0, mCurrentViewNumber); } } public void setAdapter(Adapter adapter) { mAdapter = adapter; mCurrentPosition = 0; mCurrentViewNumber = 0; // Load the initial views from adapter mViews[0].recycleView(mCurrentPosition); mViews[1].recycleView(getNextPosition(mCurrentPosition)); mViews[2].recycleView(getPrevPosition(mCurrentPosition)); // Position views at correct starting offsets mViews[0].setOffset(0, 0, mCurrentViewNumber); mViews[1].setOffset(0, 0, mCurrentViewNumber); mViews[2].setOffset(0, 0, mCurrentViewNumber); } private int getViewOffset(int viewNumber, int relativeViewNumber) { // Determine width including configured padding width int offsetWidth = mGalleryWidth + mViewPaddingWidth; // Position the previous view one measured width to left if (viewNumber == getPrevViewNumber(relativeViewNumber)) { return offsetWidth; } // Position the next view one measured width to the right if (viewNumber == getNextViewNumber(relativeViewNumber)) { return offsetWidth * -1; } return 0; } void movePrevious() { // Note: We prevent movement if an animation is running so that we don't // get too ahead of ourselves. We could allow it but given that a second // movement while an existing animation is in progress would involve the // off-screen view we would need to update the animation code as well if (false == mAnimation.mIsRunningAnimation) { // Slide to previous view mFlingDirection = 1; mFlingVelocity = 0.0f; processGesture(); } } void moveNext() { if (false == mAnimation.mIsRunningAnimation) { // Slide to next view mFlingDirection = -1; mFlingVelocity = 0.0f; processGesture(); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: movePrevious(); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: moveNext(); return true; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: } return super.onKeyDown(keyCode, event); } public boolean onGalleryTouchEvent(MotionEvent event) { boolean consumed = mGestureDetector.onTouchEvent(event); if (event.getAction() == MotionEvent.ACTION_UP) { if (mIsTouched || mIsDragging) { processScrollSnap(); processGesture(); } } return consumed; } void processGesture() { int newViewNumber = mCurrentViewNumber; int reloadViewNumber = 0; int reloadPosition = 0; mIsTouched = false; mIsDragging = false; if (mFlingDirection > 0) { if (mCurrentPosition > getFirstPosition() || mIsGalleryCircular == true) { // Determine previous view and outgoing view to recycle newViewNumber = getPrevViewNumber(mCurrentViewNumber); mCurrentPosition = getPrevPosition(mCurrentPosition); reloadViewNumber = getNextViewNumber(mCurrentViewNumber); reloadPosition = getPrevPosition(mCurrentPosition); } } if (mFlingDirection < 0) { if (mCurrentPosition < getLastPosition() || mIsGalleryCircular == true) { // Determine the next view and outgoing view to recycle newViewNumber = getNextViewNumber(mCurrentViewNumber); mCurrentPosition = getNextPosition(mCurrentPosition); reloadViewNumber = getPrevViewNumber(mCurrentViewNumber); reloadPosition = getNextPosition(mCurrentPosition); } } if (newViewNumber != mCurrentViewNumber) { mCurrentViewNumber = newViewNumber; // Reload outgoing view from adapter in new position mViews[reloadViewNumber].recycleView(reloadPosition); } // Ensure input focus on the current view mViews[mCurrentViewNumber].requestFocus(); // Run the slide animations for view transitions mAnimation.prepareAnimation(mCurrentViewNumber); this.startAnimation(mAnimation); } void processScrollSnap() { // Snap to next view if scrolled passed snap position float rollEdgeWidth = mGalleryWidth * mSnapBorderRatio; int rollOffset = mGalleryWidth - (int) rollEdgeWidth; int currentOffset = mViews[mCurrentViewNumber].getCurrentOffset(); if (currentOffset <= rollOffset * -1) { // Snap to previous view mFlingDirection = 1; } if (currentOffset >= rollOffset) { // Snap to next view mFlingDirection = -1; } } private class FlingGalleryView { private int mViewNumber; private FrameLayout mParentLayout; private FrameLayout mInvalidLayout = null; private LinearLayout mInternalLayout = null; private View mExternalView = null; public FlingGalleryView(int viewNumber, FrameLayout parentLayout) { mViewNumber = viewNumber; mParentLayout = parentLayout; // Invalid layout is used when outside gallery mInvalidLayout = new FrameLayout(mContext); mInvalidLayout.setLayoutParams(new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // Internal layout is permanent for duration mInternalLayout = new LinearLayout(mContext); mInternalLayout.setLayoutParams(new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); mParentLayout.addView(mInternalLayout); } public void recycleView(int newPosition) { if (mExternalView != null) { mInternalLayout.removeView(mExternalView); } if (mAdapter != null) { mExternalView = mInvalidLayout; if (newPosition >= getFirstPosition() && newPosition <= getLastPosition()) { mExternalView = mAdapter.getView(newPosition, mExternalView, mInternalLayout); } } if (mExternalView != null) { mInternalLayout.addView(mExternalView, new LinearLayout.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } } public void setOffset(int xOffset, int yOffset, int relativeViewNumber) { // Scroll the target view relative to its own position relative to currently displayed view mInternalLayout.scrollTo(getViewOffset(mViewNumber, relativeViewNumber) + xOffset, yOffset); } public int getCurrentOffset() { // Return the current scroll position return mInternalLayout.getScrollX(); } public void requestFocus() { mInternalLayout.requestFocus(); } } private class FlingGalleryAnimation extends Animation { private boolean mIsRunningAnimation; private int mRelativeViewNumber; private int mInitialOffset; private int mTargetOffset; private int mTargetDistance; public FlingGalleryAnimation() { mIsRunningAnimation = false; mRelativeViewNumber = 0; mInitialOffset = 0; mTargetOffset = 0; mTargetDistance = 0; } void prepareAnimation(int relativeViewNumber) { mRelativeViewNumber = relativeViewNumber; // Note: In this implementation the targetOffset will always be zero // as we are centering the view; we only include the calculations of // targetOffset and targetDistance for use in future implementations mInitialOffset = mViews[mRelativeViewNumber].getCurrentOffset(); mTargetOffset = getViewOffset(mRelativeViewNumber, mRelativeViewNumber); mTargetDistance = mTargetOffset - mInitialOffset; // Set animation time as proportionate to amount of scrolling required float distanceRatio = Math.abs(mTargetDistance) / (float) mDisplayWidth; int animationDuration = (int) (mAnimationDuration * distanceRatio); Interpolator interpolator = mDecelerateInterpolater; if (mFlingVelocity > 0.0f) { // Determine out current animation velocity to see if the fling velocity is faster float currentVelocity = Math.abs(mTargetDistance) / (animationDuration / 1000.0f); // If fling velocity is faster then maybe use a linear transformation if (currentVelocity < mFlingVelocity && mIsFastFlingLinear == true) { // Continue fling at linear velocity interpolator = mLinearInterpolater; } // Reset the animation duration to match the same velocity as the fling motion animationDuration = (int) (1000 * (Math.abs(mTargetDistance) / mFlingVelocity)); } // Configure base animation properties this.setDuration(animationDuration); this.setInterpolator(interpolator); } @Override protected void applyTransformation(float interpolatedTime, Transformation transformation) { // Ensure interpolatedTime does not over-shoot then calculate new offset interpolatedTime = (interpolatedTime > 1.0f) ? 1.0f : interpolatedTime; int offset = mInitialOffset + (int) (mTargetDistance * interpolatedTime); for (int viewNumber = 0; viewNumber < 3; viewNumber++) { // We only need to animate two views as the other view will always be off-screen // as long as we don't allow additional movements while an animation is in progress if ((mTargetDistance > 0 && viewNumber != getNextViewNumber(mRelativeViewNumber)) || (mTargetDistance < 0 && viewNumber != getPrevViewNumber(mRelativeViewNumber))) { mViews[viewNumber].setOffset(offset, 0, mRelativeViewNumber); } } } @Override public boolean getTransformation(long currentTime, Transformation outTransformation) { mIsRunningAnimation = true; if (super.getTransformation(currentTime, outTransformation) == false) { // Perform final adjustment to offsets to cleanup animation mViews[0].setOffset(mTargetOffset, 0, mRelativeViewNumber); mViews[1].setOffset(mTargetOffset, 0, mRelativeViewNumber); mViews[2].setOffset(mTargetOffset, 0, mRelativeViewNumber); mIsRunningAnimation = false; } // Cancel if the screen touched if (mIsTouched || mIsDragging) { mIsRunningAnimation = false; } return mIsRunningAnimation; } } private class FlingGestureDetector extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { // Stop animation mIsTouched = true; return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (mIsDragging == false) { // Stop animation mIsTouched = true; // Reconfigure scroll mIsDragging = true; mFlingDirection = 0; mFlingVelocity = 0.0f; mCurrentOffset = mViews[mCurrentViewNumber].getCurrentOffset(); } if (e2.getAction() == MotionEvent.ACTION_MOVE) { int scrollOffset = Math.round(mCurrentOffset + e1.getX() - e2.getX()); // We can't scroll more than the width of our own frame layout if (scrollOffset >= mGalleryWidth) scrollOffset = mGalleryWidth; if (scrollOffset <= mGalleryWidth * -1) scrollOffset = mGalleryWidth * -1; mViews[0].setOffset(scrollOffset, 0, mCurrentViewNumber); mViews[1].setOffset(scrollOffset, 0, mCurrentViewNumber); mViews[2].setOffset(scrollOffset, 0, mCurrentViewNumber); } return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (Math.abs(e1.getY() - e2.getY()) <= swipe_max_off_path) { if (e2.getX() - e1.getX() > swipe_min_distance && Math.abs(velocityX) > swipe_threshold_veloicty) { // Fling to previous view mFlingDirection = 1; mFlingVelocity = Math.abs(velocityX); } if(e1.getX() - e2.getX() > swipe_min_distance && Math.abs(velocityX) > swipe_threshold_veloicty) { // Fling to next view mFlingDirection = -1; mFlingVelocity = Math.abs(velocityX); } if (mFlingDirection != 0) { // Process immediately processGesture(); } } return false; } @Override public void onLongPress(MotionEvent e) { // Finalise scrolling mFlingDirection = 0; mFlingVelocity = 0; processGesture(); } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } } } --------------------------------------------------------------------------------------------------------- Class "FlingGalleryActivity" --------------------------------------------------------------------------------------------------------- package com.droidful.flinggallery; import android.app.Activity; import android.os.Bundle; import android.content.Context; import android.graphics.Color; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TableLayout; import android.widget.TextView; public class FlingGalleryActivity extends Activity { private final int color_red = Color.argb(100, 200, 0, 0); private final int color_green = Color.argb(100, 0, 200, 0); private final int color_blue = Color.argb(100, 0, 0, 200); private final int color_yellow = Color.argb(100, 200, 200, 0); private final int color_purple = Color.argb(100, 200, 0, 200); private final String[] mLabelArray = {"View1", "View2", "View3", "View4", "View5"}; private final int[] mColorArray = {color_red, color_green, color_blue, color_yellow, color_purple}; private FlingGallery mGallery; private CheckBox mCheckBox; // Note: The following handler is critical to correct function of // the FlingGallery class. This enables the FlingGallery class to // detect when the motion event has ended by finger being lifted @Override public boolean onTouchEvent(MotionEvent event) { return mGallery.onGalleryTouchEvent(event); } public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mGallery = new FlingGallery(this); mGallery.setAdapter(new ArrayAdapter<String>(getApplicationContext(), android.R.layout.simple_list_item_1, mLabelArray) { @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView != null && convertView instanceof GalleryViewItem) { GalleryViewItem galleryView = (GalleryViewItem) convertView; galleryView.mEdit1.setText(""); galleryView.mText1.setText(mLabelArray[position]); galleryView.mText1.setBackgroundColor(mColorArray[position]); galleryView.mText2.setText(mLabelArray[position]); galleryView.mText2.setBackgroundColor(mColorArray[position]); return galleryView; } return new GalleryViewItem(getApplicationContext(), position); } }); LinearLayout layout = new LinearLayout(getApplicationContext()); layout.setOrientation(LinearLayout.VERTICAL); layout.addView(mGallery, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT, 1)); mCheckBox = new CheckBox(getApplicationContext()); mCheckBox.setText("Gallery is Circular"); mCheckBox.setText("Gallery is Circular"); mCheckBox.setPadding(50, 10, 0, 10); mCheckBox.setTextSize(30); mCheckBox.setChecked(true); mCheckBox.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { mGallery.setIsGalleryCircular(mCheckBox.isChecked()); } }); layout.addView(mCheckBox, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); setContentView(layout); } private class GalleryViewItem extends TableLayout { private EditText mEdit1; private TextView mText1; private TextView mText2; private Button mButton1; private Button mButton2; public GalleryViewItem(Context context, int position) { super(context); this.setOrientation(LinearLayout.VERTICAL); this.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)); mEdit1 = new EditText(context); this.addView(mEdit1, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); mText1 = new TextView(context); mText1.setText(mLabelArray[position]); mText1.setTextSize(30); mText1.setGravity(Gravity.LEFT); mText1.setBackgroundColor(mColorArray[position]); this.addView(mText1, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); mButton1 = new Button(context); mButton1.setText("<<"); mButton1.setGravity(Gravity.LEFT); mButton1.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { mGallery.movePrevious(); } }); this.addView(mButton1, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); mButton2 = new Button(context); mButton2.setText(">>"); mButton2.setGravity(Gravity.RIGHT); mButton2.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { mGallery.moveNext(); } }); this.addView(mButton2, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); mText2 = new TextView(context); mText2.setText(mLabelArray[position]); mText2.setTextSize(30); mText2.setGravity(Gravity.RIGHT); mText2.setBackgroundColor(mColorArray[position]); this.addView(mText2, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT, 1)); } } } --------------------------------------------------------------------------------------------------------- Hope someone finds it useful. Regards -- You received this message because you are subscribed to the Google Groups "Android Developers" group. To post to this group, send email to android-developers@googlegroups.com To unsubscribe from this group, send email to android-developers+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/android-developers?hl=en