Aller au contenu

CarouselLayout


Cuillère

Recommended Posts

Bonjour à tous !

Je souhaite partager mon travail pour afficher un Carrousel sur Android.

Voilà le dépôt git qui contient le code source du layout ainsi qu'un projet d'exemple :

git@bitbucket.org:abecker67/repo.git (https://bitbucket.org/abecker67/repo)

J'attends vos remarques / impressions (voir votre participation) pour améliorer le projet ! ;)

Modifié par Abecker
Lien vers le commentaire
Partager sur d’autres sites

Mise à jour du carousel pour régler quelques problèmes (disponible sur le repo git)

J'ajoute un screen du projet d'exemple.

/*Copyright 2012 Arthur Becker
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.abecker.androidutils.ui;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Matrix;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Transformation;
import android.widget.Adapter;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
public class CarouselLayout extends ViewGroup {
String tag = getClass().getName();
Matrix mMatrix;
// Animations
private final int MAX_VELOCITY = 5000;
private final int ANIMATION_DURATION = 250;
private boolean isAnimating = false;
private boolean isAnimatingToCenter = false;
private float velocity = 0;
private int velocityFactor = 1;
private long lastCurTime = 0;
private long curTime = 0;
private long startTime = 0;
private float startScrollOffset = 0;
private float endScrollOffset = 0;
private long elapsedTime = 0;
// Available via getters and setters
OnItemSelectedListener onItemSelectedListener;
OnItemClickListener onItemClickListener;
ChildStaticTransformationDatasource childStaticTransformationDatasource;
Adapter adapter;
boolean animateToCenter = false;
private int mChildrenLimit = 2;
private int mChildWidth = 100;
private int mChildHeight = 100;
private int mFirstIndex = Integer.MAX_VALUE;
private int mLastIndex = Integer.MIN_VALUE;
// View collector for recycle views
private Collector mCollector;
// Center X of the view
private int mWidthCenter = 0;
// Center Y of the view
private int mHeightCenter = 0;
// Currently selected index of the adapter
private int selection = 0;
// Children total width
private int mChildrenTotalWidth = 0;
// Scroll offset (0 when not animating)
private float mScrollOffset = 0;
// Center X of the first child (used for collecting old views)
private float mMaxLeftChildCenter = 0;
// Center X of the last child (used for collecting old views)
private float mMaxRightChildCenter = 0;
public CarouselLayout(Context context) {
this(context, null);
}
public CarouselLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mCollector = new Collector();
setStaticTransformationsEnabled(true);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// isAnimating = false;
mGestureDetector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP && !isAnimating) {
 startTime = System.currentTimeMillis();
 startScrollOffset = mScrollOffset;
 if (velocityFactor == -1)
 endScrollOffset = 0;
 else
 endScrollOffset = mChildWidth - 1;
 if (animateToCenter)
 post(centerAnimation);
}
return true;
}
});
}
// Animation onFling
private Runnable animation = new Runnable() {
@Override
public void run() {
curTime = System.currentTimeMillis();
elapsedTime = (int) (curTime - lastCurTime);
scrollBy(velocity * ((float) elapsedTime / 1000f) * velocityFactor);
decelerateAnimation(elapsedTime);
lastCurTime = curTime;
if (velocity <= 0) {
startTime = System.currentTimeMillis();
startScrollOffset = mScrollOffset;
if (velocityFactor == -1)
 endScrollOffset = 0;
else
 endScrollOffset = mChildWidth - 1;
if (animateToCenter)
 post(centerAnimation);
else
 isAnimating = false;
} else if (isAnimating)
post(this);
}
};
// Animation onFling ends || onTouch up
private Runnable centerAnimation = new Runnable() {
@Override
public void run() {
isAnimatingToCenter = true;
curTime = System.currentTimeMillis();
elapsedTime = curTime - startTime;
if (elapsedTime > ANIMATION_DURATION) {
// animation end
if (velocityFactor == -1)
 scrollBy(-mScrollOffset);
else
 scrollBy(mChildWidth - mScrollOffset);
isAnimating = false;
isAnimatingToCenter = false;
notifyOnItemSelectedListener();
} else {
float percent = (float) elapsedTime / (float) ANIMATION_DURATION;
float nextFrameScrollOffset = endScrollOffset - (1f - percent) * (endScrollOffset - startScrollOffset);
scrollBy(nextFrameScrollOffset - mScrollOffset);
post(this);
}
}
};
// Velocity deceleration alg
private void decelerateAnimation(long elapsedTime) {
velocity -= elapsedTime * 5;
if (velocity < 0)
velocity = 0;
}
// GestureDetector which manages (1)animation launch in onFling and
// (2)onscroll
GestureDetector mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
isAnimating = true;
velocity = Math.min(Math.abs(velocityX), MAX_VELOCITY);
if (velocityX < 0)
velocityFactor = 1;
else
velocityFactor = -1;
lastCurTime = System.currentTimeMillis();
post(animation);
return true;
};
public boolean onscroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceX) > Math.abs(distanceY) && !isAnimating) {
scrollBy(distanceX);
if (distanceX > 0)
 velocityFactor = 1;
else
 velocityFactor = -1;
return true;
}
return false;
};
public boolean onSingleTapConfirmed(MotionEvent e) {
int index = 0;
float x = e.getX() + mScrollOffset;
float distanceFromCenter = x - mWidthCenter - mChildWidth / 2;
index = (int) (distanceFromCenter / mChildWidth);
if (distanceFromCenter > 0)
index++;
notifyOnItemClickListener(rawIndexToIndex(selection + index));
return true;
};
});
// Scroll horizontaly the view by the specified offset.
// It manages view recycling and mSelectedIndex updates
private void scrollBy(float scroll) {
int selectionOffset = (int) (scroll / (float) mChildWidth) + 1;
if (selectionOffset > 1) {
scroll = mChildWidth - 1;
}
mScrollOffset += scroll;
int lastSelected = selection;
childrenLayout();
invalidate();
for (int i = 0; i < getChildCount(); i++) {
getChildAt(i).invalidate();
}
if (!isAnimatingToCenter) {
if (lastSelected == rawIndexToIndex(selection - 1)) {
// recycle children on left
View old = getChildAt(0);
removeView(old);
mCollector.collect(old);

// add children on right
View retrieve = mCollector.retrieve();
View child = adapter.getView(rawIndexToIndex(selection + mChildrenLimit), retrieve, this);
addView(child);
mLastIndex = rawIndexToIndex(mLastIndex + selectionOffset);
mFirstIndex = rawIndexToIndex(mFirstIndex + selectionOffset);
mScrollOffset -= mChildWidth * selectionOffset;
maxCenterBounds();
}
if (lastSelected == rawIndexToIndex(selection + 1)) {
// recycle one child on right
View old = getChildAt(getChildCount() - 1);
removeView(old);
mCollector.collect(old);

// add one child on left
View retrieve = mCollector.retrieve();
View child = adapter.getView(rawIndexToIndex(selection - mChildrenLimit), retrieve, this);
addView(child, 0);

mFirstIndex = rawIndexToIndex(mFirstIndex - 1);
mLastIndex = rawIndexToIndex(mLastIndex - 1);
mScrollOffset += mChildWidth;
maxCenterBounds();
}
// Log.d(tag, "first " + mFirstIndex + " last " + mLastIndex);
}
}
private void notifyOnItemSelectedListener() {
if (onItemSelectedListener != null)
onItemSelectedListener.onItemSelected(null, this, selection, -1L);
}
private void notifyOnItemClickListener(int index) {
if (onItemClickListener != null)
onItemClickListener.onItemClick(null, this, index, -1L);
}
// Get a valide index with a raw index (e.g : rawIndex -1 becomes
// adapter.getCount-1)
private int rawIndexToIndex(int rawIndex) {
int index = rawIndex;
if (rawIndex < 0)
index += adapter.getCount();
if (rawIndex >= adapter.getCount())
index -= adapter.getCount();
return index;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int B) {
childrenLayout();
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(mChildWidth, mChildHeight);
}
// Layout all the children of the view
private void childrenLayout() {
mChildrenTotalWidth = mChildWidth * (mChildrenLimit * 2 + 1);
final int startLeftForChildren = mWidthCenter - mChildrenTotalWidth / 2 - (int) mScrollOffset;
int currentLeftForChildren = startLeftForChildren;
final int childTop = mHeightCenter - mChildHeight / 2;
final int childBottom = childTop + mChildHeight;
mMaxLeftChildCenter = Float.MAX_VALUE;
mMaxRightChildCenter = Float.MIN_VALUE;
int curChildCenterWidth = 0;
int selectedChildCenterWidth = 0;
int selectedChildIndex = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(currentLeftForChildren, childTop, currentLeftForChildren + mChildWidth, childBottom);
curChildCenterWidth = currentLeftForChildren + mChildWidth / 2;
if (Math.abs(curChildCenterWidth - mWidthCenter) <= Math.abs(selectedChildCenterWidth - mWidthCenter)) {
selectedChildIndex = i;
selectedChildCenterWidth = curChildCenterWidth;
}
currentLeftForChildren += mChildWidth;
}
selection = rawIndexToIndex(mFirstIndex + selectedChildIndex);
notifyOnItemSelectedListener();
maxCenterBounds();
}
// Refreshes max center bounds of children
private void maxCenterBounds() {
mMaxLeftChildCenter = getChildAt(0).getLeft() + mChildWidth;
mMaxRightChildCenter = getChildAt(getChildCount() - 1).getLeft() + mChildWidth;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
measureChildren(mChildWidth, mChildHeight);
}
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
}
mWidthCenter = result / 2;
return result;
}
private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
}
mHeightCenter = result / 2;
return result;
}
public Adapter getAdapter() {
return adapter;
}
public void setAdapter(Adapter adapter) {
this.adapter = adapter;
mFirstIndex = firstChildIndex();
mLastIndex = lastChildIndex();
for (int i = mFirstIndex; i != mLastIndex + 1; i = rawIndexToIndex(i + 1)) {
Log.d(tag, "add view " + i);
addView(adapter.getView(i, null, this));
}
notifyOnItemSelectedListener();
childrenLayout();
}
private int firstChildIndex() {
int first = selection - mChildrenLimit;
if (mMaxLeftChildCenter > 0)
first--;
if (first < 0)
first += adapter.getCount();
return first;
}
private int lastChildIndex() {
int last = selection + mChildrenLimit;
if (mMaxRightChildCenter < getWidth())
last++;
if (last >= adapter.getCount())
last -= adapter.getCount();
return last;
}
@Override
protected boolean getChildStaticTransformation(View child, Transformation t) {
if (childStaticTransformationDatasource == null)
return false;
t.clear();
float distanceFromCenter = child.getLeft() + mChildWidth / 2 - mWidthCenter;
t.set(childStaticTransformationDatasource.getChildStaticTransformation(this, child, distanceFromCenter));
return true;
/* return true; */
}
public int getChildrenLimit() {
return mChildrenLimit;
}
public void setChildrenLimit(int childrenLimit) {
this.mChildrenLimit = childrenLimit;
}
public int getChildWidth() {
return mChildWidth;
}
public void setChildWidth(int childWidth) {
this.mChildWidth = childWidth;
}
public int getChildHeight() {
return mChildHeight;
}
public void setChildHeight(int childHeight) {
this.mChildHeight = childHeight;
}
public boolean isAnimateToCenter() {
return animateToCenter;
}
public void setAnimateToCenter(boolean animateToCenter) {
this.animateToCenter = animateToCenter;
}
public int getSelection() {
return selection;
}
public void setSelection(int selection) {
this.selection = selection;
}
public OnItemSelectedListener getOnItemSelectedListener() {
return onItemSelectedListener;
}
public void setOnItemSelectedListener(OnItemSelectedListener onItemSelectedListener) {
this.onItemSelectedListener = onItemSelectedListener;
}
public OnItemClickListener getOnItemClickListener() {
return onItemClickListener;
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
public ChildStaticTransformationDatasource getChildStaticTransformationDatasource() {
return childStaticTransformationDatasource;
}
public void setChildStaticTransformationDatasource(ChildStaticTransformationDatasource childStaticTransformationDatasource) {
this.childStaticTransformationDatasource = childStaticTransformationDatasource;
}
private class Collector {
ArrayList<View> mOldViews;
public Collector() {
mOldViews = new ArrayList<View>();
}
public void collect(View v) {
mOldViews.add(v);
}
public View retrieve() {
if (mOldViews.size() == 0)
return null;
else
return mOldViews.remove(0);
}
}
public interface ChildStaticTransformationDatasource {
public Transformation getChildStaticTransformation(ViewGroup parent, View child, float distanceFromCenter);
}
}

Voilà le code pour ceux qui n'utilisent pas git ;)

J'ai uploadé le projet de démo ici : terafiles.net/v-158008.html

Modifié par Abecker
  • Like 1
Lien vers le commentaire
Partager sur d’autres sites

Rejoignez la conversation

Vous pouvez poster maintenant et vous enregistrez plus tard. Si vous avez un compte, connectez-vous maintenant pour poster.

Invité
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Répondre à ce sujet…

×   Collé en tant que texte enrichi.   Coller en tant que texte brut à la place

  Seulement 75 émoticônes maximum sont autorisées.

×   Votre lien a été automatiquement intégré.   Afficher plutôt comme un lien

×   Votre contenu précédent a été rétabli.   Vider l’éditeur

×   Vous ne pouvez pas directement coller des images. Envoyez-les depuis votre ordinateur ou insérez-les depuis une URL.

×
×
  • Créer...