I am investigating Android ViewPager animation in my current project.
I would like to give my users an affordance in relation to my ViewPager.
The effect I am looking for it to "shake" the ViewPager from left to right to partially show the neighbouring pages to the selected page.
When the user selects the first page (F), I would "Shake" between F and F + 1.
When the user selects the last page (L), I would "Shake" between L and L - 1.
Otherwise, when the user selects any other page (X), I would "Shake" between X - 1, X and X + 1.
I have tried the following approaches which have all given unsatisfactory results.
Fake drag using screen density to calculate shake distance and valueAnimator and ObjectAnimator.
Animating ViewPager.PageTransformer.
I am unable to get a satisfactory "Shake" effect.
I would like to achieve a Spring Dampening style of animation.
The FakeDrag was closest, although the effect was not reliable, it seemed to never work correctly the first time I started the animation.
Even the FakeDrag would not give the desired smooth shake between page swipes.
I would like to employ the physics-based animations on my view pager but couldn't see how.
Is it possible to achieve my desired animation?
UPDATE
I have developed this code which gives the desired effect, however its JANKY!
#Override
protected void onResume() {
super.onResume();
final Handler handler = new Handler();
handler.postDelayed(() -> animateViewPager(ANIMATION_OFFSET, ANIMATION_DURATION), ANIMATION_DELAY);
}
/**
* #param offset
* #param duration
*/
private void animateViewPager(final int offset, final int duration) {
if (animator.isRunning()) {
return;
}
animator.removeAllUpdateListeners();
animator.removeAllListeners();
animator.setIntValues(0, -offset);
animator.setDuration(duration);
animator.setRepeatCount(getRepeatCount());
animator.setRepeatMode(ValueAnimator.RESTART);
animator.addUpdateListener(constructUpdateListener());
animator.addListener(constructAnimatorListener());
animator.start();
}
/**
*
* #return
*/
private Animator.AnimatorListener constructAnimatorListener() {
return new AnimatorListenerAdapter() {
#Override
public void onAnimationStart(final Animator animation) {
animFactor = 1;
}
#Override
public void onAnimationEnd(final Animator animation) {
viewPager.endFakeDrag();
if (xAnimation == null) {
xAnimation = createSpringAnimation(viewPager, SpringAnimation.X, 0, SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
} else {
xAnimation.cancel();
}
viewPager.animate().x(viewPager.getWidth() * 0.1f).setDuration(0).start();
xAnimation.start();
}
#Override
public void onAnimationRepeat(final Animator animation) {
animFactor = -1;
}
};
}
/**
* #return
*/
private int getRepeatCount() {
if (isOnlyPage()) {
return 0;
}
if (isFirstListItem()) {
return REPEAT_ONCE;
}
if (isLastListItem()) {
return REPEAT_ONCE;
}
return REPEAT_TWICE;
}
#SuppressWarnings("StatementWithEmptyBody")
private ValueAnimator.AnimatorUpdateListener constructUpdateListener() {
return animation -> {
final Integer value = animFactor * (Integer) animation.getAnimatedValue();
if (viewPager.isFakeDragging()) {
} else {
viewPager.beginFakeDrag();
}
viewPager.fakeDragBy(value);
};
}
/**
* #param view
* #param property
* #param finalPosition
* #param stiffness
* #param dampingRatio
* #return
*/
private SpringAnimation createSpringAnimation(final View view, final DynamicAnimation.ViewProperty property, final float finalPosition, final float stiffness, final float dampingRatio) {
final SpringAnimation animation = new SpringAnimation(view, property);
final SpringForce springForce = new SpringForce(finalPosition);
springForce.setStiffness(stiffness);
springForce.setDampingRatio(dampingRatio);
animation.setSpring(springForce);
return animation;
}
Related
To gain experience in android I'm making a decisionmaking app. I'm trying to add a spinning wheel (wheel of fortune like). I have an animation that works fine (just need some little tweaking but fine for now). The problem I'm having is to detect where the wheel has stopped. If I look at the rotation and match it to precalculated values it makes sense, but visually it seems off.
For example:
Begin state is always like this. I tap it and it starts to rotate.
It stops rotating at red (visually), but in the model it says 51, hence my model detects it stopped rotating at yellow:
Cur pos and Prev pos is not currently implemented. Rnd pos is the relative number of degrees it needs to rotate (relative to 0). Rnd rot is number of extra rotations it has to make before stopping. True pos is the absolute number of degrees it has to rotate. Total time: the time the animation takes. Corner: number of degrees one piece has.
SpinWheel method in activity:
private void spinWheel() {
Roulette r = Roulette.getInstance();
r.init(rouletteItemsDEBUG);
r.spinRoulette();
final int truePos = r.getTruePosition();
final long totalTime = r.getTotalTime();
final ObjectAnimator anim = ObjectAnimator.ofFloat(imgSpinningWheel, "rotation", 0, truePos);
anim.setDuration(totalTime);
anim.setInterpolator(new DecelerateInterpolator());
anim.addListener(new Animator.AnimatorListener() {
#Override
public void onAnimationStart(Animator animation) {
imgSpinningWheel.setEnabled(false);
}
#Override
public void onAnimationEnd(Animator animation) {
imgSpinningWheel.setEnabled(true);
txtResult.setText(Roulette.getInstance().getSelectedItem().getValue());
Log.d(TAG, Roulette.getInstance().toString());
Log.d(TAG, imgSpinningWheel.getRotation() + "");
Log.d(TAG, imgSpinningWheel.getRotationX() + "");
Log.d(TAG, imgSpinningWheel.getRotationY() + "");
}
#Override
public void onAnimationCancel(Animator animation) {
}
#Override
public void onAnimationRepeat(Animator animation) {
}
});
anim.start();
}
Roulette.java:
public class Roulette {
private static Roulette instance;
private static final long TIME_IN_WHEEL = 1000; //time in one rotation (speed of rotation)
private static final int MIN_ROT = 5;
private static final int MAX_ROT = 6;
private static final Random rnd = new Random();
private RouletteItem currentItem;
private RouletteItem[] rouletteItems;
private NavigableMap<Integer, RouletteItem> navigableMap;
private int randomRotation;
private int randomPosition;
private int truePosition;
private int corner;
private long totalTime;
private Roulette() {
}
public void init(RouletteItem[] rouletteItems) {
this.rouletteItems = rouletteItems;
navigableMap = new TreeMap<>();
for (int i = 0; i < getNumberOfItems(); i++) {
navigableMap.put(i * 51, rouletteItems[i]);
}
}
public void spinRoulette() {
if (rouletteItems == null || rouletteItems.length < 2) {
throw new RouletteException("You need at least 2 rouletteItems added to the wheel!");
}
calculateCorner();
calculateRandomPosition();
calculateRandomRotation();
calculateTruePosition();
calculateTotalTime();
}
/**
* Pinpoint random position in the wheel
*/
private void calculateRandomPosition() {
randomPosition = corner * rnd.nextInt(getNumberOfItems());
}
/**
* Calculates the points one RouletteItem has
*/
private void calculateCorner() {
corner = 360 / getNumberOfItems();
}
/**
* Calculates the time needed to spin to the chosen random position
*/
private void calculateTotalTime() {
totalTime = (TIME_IN_WHEEL * randomRotation + (TIME_IN_WHEEL / 360) * randomPosition);
}
/**
* Calculates random rotation
*/
private void calculateRandomRotation() {
randomRotation = MIN_ROT + rnd.nextInt(MAX_ROT - MIN_ROT);
}
/**
* Calculates the true position
*/
private void calculateTruePosition() {
truePosition = (randomRotation * 360 + randomPosition);
}
//getters and to string omitted
}
The map is used to map rouletteItems to a range of degrees.
My guess is that the animation takes too long or something like that. But I don't really see how to fix it. Anyone who does?
Thanks in advance!
Um, doesn't Java implement rotation in radians rather than degrees?
That could lead to a offset between the visual rotation graphic and the number from the maths. Perhaps check by replacing all the assumed degree calculations (ie x/360) with radian calcs (ie. x/(2*pi))?
I have an custom widget which has an clear button set as the right drawable using setCompoundDrawablesWithIntrinsicBounds() method.
Now I need to add errors to the widget and I've tried to use the default functionality and rely on the setError() method.
The issue I'm having is that after I set the error, my right drawable won't be shown again, even if I set it again afterwards.
Does anyone encountered this issue and maybe found an solution to the problem?
Thank you.
LE:
I forgot to mention that if I use setError("My error hint text", null) instead of setError("Please choose an valid destination!") everything works fine, but as you can guess the error icon won't be shown.
LLE:
This is my custom class which handles the display of the clear icon and the action for it.
/**
* Class of {#link android.widget.AutoCompleteTextView} that includes a clear (dismiss / close) button with a
* OnClearListener to handle the event of clicking the button
* <br/>
* Created by ionut on 26.04.2016.
*/
public class ClearableAutoCompleteTextView extends AppCompatAutoCompleteTextView implements View.OnTouchListener {
/**
* The time(in milliseconds) used for transitioning between the supported states.
*/
private static final int TRANSITION_TIME = 500;
/**
* Flag for hide transition.
*/
private static final int TRANSITION_HIDE = 101;
/**
* Flag for show transition.
*/
private static final int TRANSITION_SHOW = 102;
/**
* Image representing the clear button. (will always be set to the right of the view).
*/
#DrawableRes
private static int mImgClearButtonRes = R.drawable.ic_clear_dark;
/**
* Task with the role of showing the clear action button.
*/
private final Runnable showClearButtonRunnable = new Runnable() {
#Override
public void run() {
Message msg = transitionHandler.obtainMessage(TRANSITION_SHOW);
msg.sendToTarget();
}
};
/**
* Task with the role of hiding the clear action button.
*/
private final Runnable hideClearButtonRunnable = new Runnable() {
#Override
public void run() {
Message msg = transitionHandler.obtainMessage(TRANSITION_HIDE);
msg.sendToTarget();
}
};
/**
* Flag indicating if the default clear functionality should be disabled.
*/
private boolean mDisableDefaultFunc;
// The default clear listener which will clear the input of any text
private OnClearListener mDefaultClearListener = new OnClearListener() {
#Override
public void onClear() {
getEditableText().clear();
setText(null);
}
};
/**
* Touch listener which will receive any touch events that this view handles.
*/
private OnTouchListener mOnTouchListener;
/**
* Custom listener which will be notified when an clear event was made from the clear button.
*/
private OnClearListener onClearListener;
/**
* Transition drawable used when showing the clear action.
*/
private TransitionDrawable mTransitionClearButtonShow = null;
/**
* Transition drawable used when hiding the clear action.
*/
private TransitionDrawable mTransitionClearButtonHide = null;
private AtomicBoolean isTransitionInProgress = new AtomicBoolean(false);
private final Handler transitionHandler = new Handler(Looper.getMainLooper()) {
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case TRANSITION_HIDE:
setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
isTransitionInProgress.set(false);
break;
case TRANSITION_SHOW:
setCompoundDrawablesWithIntrinsicBounds(0, 0, mImgClearButtonRes, 0);
isTransitionInProgress.set(false);
break;
default:
super.handleMessage(msg);
}
}
};
/* Required methods, not used in this implementation */
public ClearableAutoCompleteTextView(Context context) {
super(context);
init();
}
/* Required methods, not used in this implementation */
public ClearableAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/* Required methods, not used in this implementation */
public ClearableAutoCompleteTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
#Override
public boolean onTouch(View v, MotionEvent event) {
if (getCompoundDrawables()[2] == null) {
// Pass the touch event to other listeners, if none, the normal flow is resumed
return null != mOnTouchListener && mOnTouchListener.onTouch(v, event);
}
// React only when the UP event is detected
if (event.getAction() != MotionEvent.ACTION_UP) {
return false;
}
// Detect the clear button area of touch
int x = (int) event.getX();
int y = (int) event.getY();
int left = getWidth() - getPaddingRight() - getCompoundDrawables()[2].getIntrinsicWidth();
int right = getWidth();
boolean tappedX = x >= left && x <= right && y >= 0 && y <= (getBottom() - getTop());
if (tappedX) {
// Allow clear events only when the transition is not in progress
if (!isTransitionInProgress.get()) {
if (!mDisableDefaultFunc) {
// Call the default functionality only if it wasn't disabled
mDefaultClearListener.onClear();
}
if (null != onClearListener) {
// Call the custom clear listener so that any member listening is notified of the clear event
onClearListener.onClear();
}
}
}
// Pass the touch event to other listeners, if none, the normal flow is resumed
return null != mOnTouchListener && mOnTouchListener.onTouch(v, event);
}
#Override
public void setOnTouchListener(OnTouchListener l) {
// Instead of using the super, we manually handle the touch event (only one listener can exist normally at a
// time)
mOnTouchListener = l;
}
private void init() {
// Set the bounds of the button
setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
if (getCompoundDrawablePadding() == 0) {
// We want to have some default padding, in case no one is specified in the xml
setCompoundDrawablePadding(Dimensions.dpToPx(getContext(), 5f));
}
enableTransitionClearButton();
// if the clear button is pressed, fire up the handler. Otherwise do nothing
super.setOnTouchListener(this);
}
#Override
public void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
updateClearButton();
}
#Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
updateClearButton();
}
/**
* When hiding/showing the clear button an transition drawable will be used instead of the default drawable res.
*/
public void enableTransitionClearButton() {
mTransitionClearButtonShow =
(TransitionDrawable) ContextCompat.getDrawable(getContext(), R.drawable.ic_clear_fade_in);
mTransitionClearButtonHide =
(TransitionDrawable) ContextCompat.getDrawable(getContext(), R.drawable.ic_clear_fade_out);
mTransitionClearButtonShow.setCrossFadeEnabled(true);
mTransitionClearButtonHide.setCrossFadeEnabled(true);
}
/**
* When hiding/showing the clear button the default drawable res will be used instead of the transition drawable.
*/
#SuppressWarnings("unused")
public void disableTransitionClearButton() {
mTransitionClearButtonShow = null;
isTransitionInProgress.set(false);
transitionHandler.removeCallbacks(hideClearButtonRunnable);
transitionHandler.removeCallbacks(showClearButtonRunnable);
}
/**
* Set an custom listener which will get notified when an clear event was triggered from the clear button.
*
* #param clearListener
* The listener
* #param disableDefaultFunc
* {#code true} to disable the default clear functionality, usually meaning it will be
* handled by the
* calling member. {#code false} allow the default functionality of clearing the input.
*
* #see #setOnClearListener(OnClearListener)
*/
public void setOnClearListener(final OnClearListener clearListener, boolean disableDefaultFunc) {
this.onClearListener = clearListener;
this.mDisableDefaultFunc = disableDefaultFunc;
}
/**
* Set an custom listener which will get notified when an clear event was triggered from the clear button.
*
* #param clearListener
* The listener
*
* #see #setOnClearListener(OnClearListener, boolean)
*/
public void setOnClearListener(final OnClearListener clearListener) {
setOnClearListener(clearListener, false);
}
/**
* Disable the default functionality of the clear event - calling this won't allow for the input to be
* automatically
* cleared (it will give the ability to make custom implementations and react to the event before the clear).
*/
#SuppressWarnings("unused")
public void disableDefaultClearFunctionality() {
mDisableDefaultFunc = true;
}
/**
* Enable the default functionality of the clear event. Will automatically clear the input when the clear button is
* clicked.
*/
#SuppressWarnings("unused")
public void enableDefaultClearFunctionality() {
mDisableDefaultFunc = false;
}
/**
* Automatically show/hide the clear button based on the current input detected.
* <br/>
* If there is no input the clear button will be hidden. If there is input detected the clear button will be shown.
*/
public void updateClearButton() {
if (isEmpty()) {
hideClearButton();
} else {
showClearButton();
}
}
private boolean isEmpty() {
if (null == getText()) {
// Invalid editable text
return true;
} else if (TextUtils.isEmpty(getText().toString())) {
// Empty
return true;
} else if (TextUtils.isEmpty(getText().toString().trim())) {
// White spaces only
return true;
}
return false;
}
/**
* Hide the clear button.
* <br/>
* If an transition drawable was provided, it will be used to create an fade out effect, otherwise the default
* drawable resource will be used.
*/
public void hideClearButton() {
if (getCompoundDrawables()[2] == null) {
// The clear button was already hidden - do nothing
return;
}
if (null == mTransitionClearButtonHide) {
setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
return;
}
mTransitionClearButtonHide.resetTransition();
isTransitionInProgress.set(true);
mTransitionClearButtonHide.startTransition(TRANSITION_TIME);
setCompoundDrawablesWithIntrinsicBounds(null, null, mTransitionClearButtonHide, null);
transitionHandler.removeCallbacks(showClearButtonRunnable);
transitionHandler.removeCallbacks(hideClearButtonRunnable);
transitionHandler.postDelayed(hideClearButtonRunnable, TRANSITION_TIME);
}
/**
* Show the clear button.
* <br/>
* If an transition drawable was provided, it will be used to create an fade in effect, otherwise the default
* drawable resource will be used.
*/
public void showClearButton() {
if (getCompoundDrawables()[2] != null) {
// The clear button was already set - do nothing
return;
}
if (null == mTransitionClearButtonShow) {
setCompoundDrawablesWithIntrinsicBounds(0, 0, mImgClearButtonRes, 0);
return;
}
isTransitionInProgress.set(true);
mTransitionClearButtonShow.startTransition(TRANSITION_TIME);
setCompoundDrawablesWithIntrinsicBounds(null, null, mTransitionClearButtonShow, null);
transitionHandler.removeCallbacks(hideClearButtonRunnable);
transitionHandler.removeCallbacks(showClearButtonRunnable);
transitionHandler.postDelayed(showClearButtonRunnable, TRANSITION_TIME);
}
/**
* Custom contract which is used to notify any listeners that an clear event was triggered.
*/
public interface OnClearListener {
/**
* Clear event from the clear button triggered.
*/
void onClear();
}
}
When I want to display the error I use the following command:
// Enable and show the error
mClearView.requestFocus(); // we need to request focus, otherwise the error won't be shown
mClearView.setError("Please choose an valid destination!", null);
If I use:
mClearView.setError("Please choose an valid destination!"), so that the error icon to be shown, the clear button won't be shown anymore after this.
Since the documentation of setCompoundDrawablesWithIntrinsicBounds() (not so) clearly states that subsequent calls should overwrite the previously set drawables, and the documentation of setError() says the following:
Sets the right-hand compound drawable of the TextView to the "error" icon and sets an error message that will be displayed in a
popup when the TextView has focus.
I assume this is a bug.
Calling setError(null) and setting the previously set drawables to null before setting your new drawable is a possible workaround:
void setError(String error) {
editText.setError(error);
}
void setCompoundDrawableRight(Drawable rightDrawable) {
editText.setError(null);
editText.setCompoundDrawables(null, null, null, null);
editText.setCompoundDrawablesWithIntrinsicBounds(null, null, rightDrawable, null);
}
You must have used the facebook android app. I want to implement the same navigation that the facebook is using like:
on swipping left the menu is opened and swipping right the chat list is shown. And in the middle the activities and layouts keep on changing. But I am confused how to make such a navigation. (Most noticable thing is that the middle page is half shown when the swipping is done on left or right.) help?
You have two good options.
Navigation Drawer: http://developer.android.com/design/patterns/navigation-drawer.html
Sliding Drawer: https://github.com/jfeinstein10/SlidingMenu
You can make multiple navigation drawers for your use case.
I installed the FB app an hour ago. When you tap the icon in the top-right corner of the main content, the main content moves left to reveal content on the right. The revealed content remains fixed and behind the main content during the move.
I wrote this lightweight abstraction based on the above analysis:
package org.yourdomain.app;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
abstract public class NavigatorActivity extends Activity {
static public final String TAG = "NavigatorActivity";
static public final Boolean DEBUG_LIFECYCLE = false;
/**
* True when menu is on and vice versa
*/
private boolean mToggled;
/**
* Percentage of the main content to keep shown
*/
private float mDistance;
/**
* Navigator listener
*/
private NavigatorListener mListener;
/**
* Content view
*/
private ViewGroup mContentLayout;
/**
* Menu frame
*/
private FrameLayout mMenuLayout;
/**
* Main frame
*/
private FrameLayout mMainLayout;
/**
* Speed of toggle animation
*/
private long mSpeed;
/**
* Width of the content view
*/
private int mContentWidth;
/**
* Height of the content view
*/
private int mContentHeight;
/**
* The current distance to slide the main frame
*/
private int mToggleDistance;
/**
*
*/
public NavigatorActivity() {
mToggled = false;
mDistance = 80;
mSpeed = 300l;
}
/**
* Speed setter.
*
* Controls how fast the menu frame is revealed.
*
* #param speed
*/
public void setSpeed(long speed) {
mSpeed = speed;
}
/**
* Distance setter.
*
* The distance is the % of the oriented screen to pull the main frame in order to reveal the
* menu frame.
*
* #param distance
*/
public void setDistance(float distance) {
mDistance = distance;
}
/**
* Navigator listener setter.
*
* #param listener
*/
public void setNavigatorListener(NavigatorListener listener) {
mListener = listener;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG_LIFECYCLE) Log.v(TAG, "onCreate " + this + ": " + savedInstanceState);
initContentLayout();
setContentView(mContentLayout);
initMenuLayout();
mContentLayout.addView(mMenuLayout);
initMainLayout();
mContentLayout.addView(mMainLayout);
}
/**
* Initializes the main frame.
*/
private void initMainLayout() {
int hw = RelativeLayout.LayoutParams.MATCH_PARENT;
mMainLayout = new FrameLayout(this) {
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(mContentWidth, mContentHeight);
}
};
RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(hw, hw);
p.addRule(RelativeLayout.ALIGN_PARENT_TOP);
p.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
mMainLayout.setLayoutParams(p);
}
/**
* Initializes the menu frame.
*/
private void initMenuLayout() {
int hw = RelativeLayout.LayoutParams.MATCH_PARENT;
mMenuLayout = new FrameLayout(this);
RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(hw, hw);
p.addRule(RelativeLayout.ALIGN_PARENT_TOP);
p.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
mMenuLayout.setLayoutParams(p);
}
/**
* Initialize the activity's content layout.
*/
private void initContentLayout() {
final int hw = RelativeLayout.LayoutParams.MATCH_PARENT;
mContentLayout = new RelativeLayout(this) {
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mContentWidth = MeasureSpec.getSize(widthMeasureSpec);
mContentHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(mContentWidth, mContentHeight);
mToggleDistance = Math.round((mDistance / 100f) * (float) mContentWidth);
}
};
mContentLayout.setLayoutParams(new RelativeLayout.LayoutParams(hw, hw));
}
/**
* Inflates two project XML layout and adds them to the menu and main layout frames.
*
* #param menuLayoutResId
* #param mainLayoutResId
*/
protected void setContentViews(int menuLayoutResId, int mainLayoutResId) {
final LayoutInflater inflater = getLayoutInflater();
inflater.inflate(menuLayoutResId, mMenuLayout);
inflater.inflate(mainLayoutResId, mMainLayout);
}
/**
* Toggle the menu frame.
*/
final public void toggleNavigator() {
final boolean isToggled = mToggled = !mToggled;
if (DEBUG_LIFECYCLE) Log.v(TAG, "toggleNavigatorMenu " + this);
RelativeLayout.LayoutParams menuParams = (RelativeLayout.LayoutParams) mMenuLayout.getLayoutParams();
menuParams.setMargins(mContentWidth - mToggleDistance, 0, 0, 0);
final RelativeLayout.LayoutParams mainParams = (RelativeLayout.LayoutParams) mMainLayout.getLayoutParams();
ValueAnimator animator = ValueAnimator.ofInt(mainParams.leftMargin, mToggled ? -mToggleDistance : 0);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mainParams.leftMargin = (Integer) valueAnimator.getAnimatedValue();
mMainLayout.requestLayout();
}
});
animator.addListener(new Animator.AnimatorListener() {
#Override
public void onAnimationStart(Animator animation) {}
#Override
public void onAnimationEnd(Animator animation) {
if (mListener != null) {
mListener.onNavigatorToggled(isToggled);
}
}
#Override
public void onAnimationCancel(Animator animation) {}
#Override
public void onAnimationRepeat(Animator animation) {}
});
animator.setDuration(mSpeed);
animator.start();
}
public static interface NavigatorListener {
public void onNavigatorToggled(boolean isToggled);
}
#Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
toggleNavigator();
}
return super.onKeyUp(keyCode, event);
}
}
Example usage:
package org.yourdomain.project;
import android.os.Bundle;
import org.yourdomain.app.NavigatorActivity;
public class YourActivity extends NavigatorActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDistance(80); // will leave 20% of the main content in view
setNavigatorListener(new NavigatorListener() {
#Override
public void onNavigatorToggled(boolean isToggled) {
// Load content dynamically, like FB does?
}
});
setContentViews(R.layout.layout_menu, R.layout.activity_your);
}
}
compileSdkVersion 20
minSdkVersion 16
targetSdkVersion 20
Clicking on the physical menu button on your Android device will toggle the navigator.
I plan on using this in a couple of startup projects of my own.
Let me know if you have any questions. I hope this helps you.
So the problem I am facing right now is the following, I want to create a fisheye view (so a view which contains items while the item in the middle is bigger then the other ones like you will see in the MAC OS itembar or somewhere else).
So far I have extended the HorizontalScrollView to achieve that and after some testing everything seemed to work fine, so when moving the scrollview the items are updated properly depending on their position.
However there is one problem that occurs if the scrollview "bounces" against its borders. So if the ScrollView moves to fast the "getScrollX()" will give me a value that is smaller 0 or bigger then the max bounds. After that happens the items are no longer resized, which is quite strange.
I checked my code and the resize methods of my items are called, yet i don´t know why the items are no longer updated.
The ScrollView class looks like the following
public class HorizontalFishEyeView extends HorizontalScrollView
{
//****************************************************************************************************
// enums
//****************************************************************************************************
public enum ONTOUCHEND
{
//-----undefined value
NONE,
//-----meaning to keep scrolling after the touch event ended
SCROLL_ON_END,
//-----meaning to continue to the next base after the touch event ended
CONTINUE_ON_END,
//-----meaning to switch the element after the on touch event ended
CHANGE_ELEMENT_ON_END
}
public enum MODE
{
//-----none mode meaning the view is not scrolling or doing the finish animation, thus being idle
NONE,
//-----scroll meaning the view is scrolling after an accelleration
SCROLL,
//-----finish meaning the view is doing the finish animation to move to the actual element
FINISH,
}
//****************************************************************************************************
// variables
//****************************************************************************************************
//-----determines if the view will continue when the finish animation is played
private boolean m_bContinueOnClick = false;
//-----time for the scroll animation
private long m_nScrollAnimationTime = 0;
//------the multiplier to be used for the velocity on initaial start
private float m_fVelocityMultiplier = 1.0f;
//-----the direction of the velocity however the reverse value to use it in conjunction with the decrement
private int m_nVelocityDirectionReverse = 0;
//------the velocity provided when the event has ended
private float m_fVelocity = 0.0f;
//-----determines hwo much the velocity decreases per millisecond
private float m_fVelocityDecrement = 0.001f;
//-----time when the touch event was started
private long m_nStartTime = 0;
//-----the x position of the touch event
private float m_nXPosition = -1.0f;
//-----determines when the animation for moving shall be canceled
private final float m_fVelocityThreshold = 0.25f;
//-----determines the time, e.g the start time of the animation and stores the time each time the draw method is called
//-----while the finish animation is in progress
private long m_nFinishAnimationTime = 0;
//-----determines how much pixel the layout will be moved for each millisecond passed,
//-----while the finish animation is playing
private double m_dFinishAnimationIncrements = 0.0;
//-----the actually duration of the finish animation, this value is dependent of the difference distance
//-----which the view has to be moved, so at max this will bei 0.5 times m_nFinishAnimationTime
private int m_nFinishAnimationDuration = 0;
//-----determines the distance which the view has to be moved in order to set the selected element into focus
private int m_nFinishRemainingDiff = 0;
//-----the position which the view will have as its left margin,
//-----this value us determined when the user lets go of the view
private int m_nFinishTargetPosition = 0;
//-----the animation time the finish animation when the user lets go of the view
private int m_nAnimationTime = 0;
//-----the position of the element which is closest to the selector, thus it actually is the selected element
private FishEyeItem m_nClosestElement = null;
//-----scalefactor used to calculate the min item size, thus m_nItemSizeMin = nItemSize * m_fItemSizeMaxScale
private float m_fItemSizeMinScale = -1;
//-----the size of the image of the item when not selected
private int m_nItemSizeMin = -1;
//-----scalefactor used to calculate the max item size, thus m_nItemSizeMax = nItemSize * m_fItemSizeMaxScale
private float m_fItemSizeMaxScale = -1;
//-----the size of the image of the item when selected
private int m_nItemSizeMax = -1;
//-----the difference in item size between the max and the min value
private int m_nItemSizeDiff = -1;
//-----determines at which distance the item size will always be min
private int m_nMaxDiff = 0;
//-----the middel point of the view, used to determine the distance of an item and thus its size
private int m_nReferenceX = 0;
//-----event listener attached to this view
protected AnimationEventListener m_oEventListener;
//-----this factor is multiplied by the velocity up on the UP event to determine the remaining scroll
private float m_fVelocityScaleFactor = 0.25f;
//-----the mode in which the fisheyeview currently is
private MODE m_eMode = MODE.NONE;
//-----the reference to the one and only child in the scrollview, as it should be
private LinearLayout m_oChild = null;
//-----number of items whose bitmap will still be available even if they are not visible
private int m_nItemBuffer = 2;
//-----activity to use
private Activity m_oActivity = null;
//-----scalefactor to use
private float m_fScaleFactor = 1.0f;
//-----determines if the itemsize is stable thus each item is the same size, used to prevent unnecessary calculations
private boolean m_bItemSizeStable = false;
//****************************************************************************************************
// constructor
//****************************************************************************************************
public HorizontalFishEyeView(Context context)
{
super(context);
}
public HorizontalFishEyeView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
//****************************************************************************************************
// public
//****************************************************************************************************
/**
* this method will set up the view before it is used, thus it needs to be called before
* #param oActivity
* #param fItemSizeMinScale
* #param fItemSizeMaxScale
* #param fScaleFactor
* #param bItemSizeStable
*/
public void Initialize(Activity oActivity, float fItemSizeMinScale, float fItemSizeMaxScale, float fScaleFactor, boolean bItemSizeStable)
{
try
{
m_oActivity = oActivity;
m_nReferenceX = (int)(getWidth()*0.5f);
m_fItemSizeMaxScale = Math.min(1.0f, Math.max(0.0f, fItemSizeMaxScale));
m_fItemSizeMinScale = Math.min(1.0f, Math.max(0.0f, fItemSizeMinScale));
m_bItemSizeStable = bItemSizeStable;
m_fScaleFactor = fScaleFactor;
}
catch(Exception e)
{
Log.d("Initialize", e.toString());
}
}
public void Clear()
{
try
{
if(m_oChild!=null)
{
for(int i=0;i<m_oChild.getChildCount();i++)
{
View oChild = m_oChild.getChildAt(i);
if(oChild instanceof FishEyeItem)
{
NetcoMethods.RecycleImageView(((FishEyeItem)oChild).GetImageView());
}
}
m_oChild.removeAllViews();
}
}
catch(Exception e)
{
Log.d("Clear", e.toString());
}
}
public void AddItem(FishEyeItem oItem, LinearLayout.LayoutParams oParams)
{
try
{
if(m_oChild!=null)
{
m_oChild.addView(oItem, oParams);
}
}
catch(Exception e)
{
Log.d("AddItem", e.toString());
}
}
public MODE GetMode()
{
return m_eMode;
}
public void Reinitialize()
{
}
public void Deinitialize()
{
}
/**
* adds an animation listener to the list
* #param listener
*/
public void SetAnimationEventListener(AnimationEventListener listener)
{
m_oEventListener = listener;
}
public void ScrollTo()
{
try
{
}
catch(Exception e)
{
Log.d("ScrollTo", e.toString());
}
}
public LinearLayout GetChild()
{
return m_oChild;
}
//****************************************************************************************************
// private
//****************************************************************************************************
/**called when the size was calculated*/
private void SizeCalculated(Object o)
{
try
{
if(m_oEventListener!=null)
{
m_oEventListener.AnimationEvent(o);
}
}
catch(Exception e)
{
Log.d("AnimationEndEvent", e.toString());
}
}
/**
* calculates the sizes for an item, if m_bItemSizeStable is set to true this will only be done once
* #param nItemSize, the size of the item which will be used
*/
private void CalulateItemSize(int nItemSize)
{
try
{
if(!m_bItemSizeStable)
{
m_nItemSizeMax = (int)(nItemSize * m_fItemSizeMaxScale);
m_nItemSizeMin = (int)(nItemSize * m_fItemSizeMinScale);
m_nItemSizeDiff = m_nItemSizeMax - m_nItemSizeMin;
m_nMaxDiff = nItemSize*2;
}
else if(m_nItemSizeMax==-1)
{
m_nItemSizeMax = (int)(nItemSize * m_fItemSizeMaxScale);
m_nItemSizeMin = (int)(nItemSize * m_fItemSizeMinScale);
m_nItemSizeDiff = m_nItemSizeMax - m_nItemSizeMin;
m_nMaxDiff = nItemSize*2;
}
}
catch(Exception e)
{
Log.d("CalculateItemSize", e.toString());
}
}
/**
* this method will Resize and item in the view depending on its position
* #param oItem the item which shall be resized
* #param nDiff the distance of the item from the middle of he view
* #param nCurrentClosestDiff the currently know closest distance, if the item is closer the given nDiff will be used
*/
private void DeterminenSize(FishEyeItem oItem, int nDiff, int nCurrentClosestDiff)
{
try
{
if(oItem!=null)
{
CalulateItemSize(oItem.getWidth());
//-----check if the item can be resized
if(oItem.GetCanBeResized())
{
//System.out.println("Item is "+ oItem.GetImagePath());
//System.out.println("Item Diff is "+ nDiff);
//-----items is in range
if(nDiff<m_nMaxDiff)
{
//-----determine whether this element is closer to the selector then the previously known
if(nCurrentClosestDiff==-1)
{
nCurrentClosestDiff = nDiff;
m_nClosestElement = oItem;
SizeCalculated(m_nClosestElement);
}
else
{
if(nDiff<nCurrentClosestDiff)
{
nCurrentClosestDiff = nDiff;
m_nClosestElement = oItem;
SizeCalculated(m_nClosestElement);
}
}
//-----get the new size
float fRelative = 1.0f - (float)nDiff/(float)m_nMaxDiff;
int nNewItemSize = m_nItemSizeMin + (int)(fRelative * m_nItemSizeDiff);
//-----set the new size
oItem.Resize(nNewItemSize, nNewItemSize);
oItem.SetIsInRange(true);
}
else
{
//----if the item is now out of range set it to the minimum size
if(oItem.GetIsInRange())
{
//-----set the minimum size
oItem.Resize(m_nItemSizeMin, m_nItemSizeMin);
oItem.SetIsInRange(false);
}
}
}
}
}
catch(Exception e)
{
Log.d("DeterminenSize", e.toString());
}
}
//****************************************************************************************************
// overrides
//****************************************************************************************************
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
{
super.onScrollChanged(l, t, oldl, oldt);
try
{
if(m_eMode == MODE.FINISH)
{
}
else
{
//------get the top element which must be a linear layout
if(m_oChild!=null)
{
m_oChild.setWillNotDraw(false);
FishEyeItem oFishEyeItem = null;
View oChildView = null;
ImageView oImage = null;
String cFilename = null;
int nPositionStart = 0;
int nPositionEnd = 0;
int nItemSize = 0;
int nScroll = getScrollX();
int nBoundEnd = getWidth();
int nItemPosition = 0;
int nCurrentClosestDiff = -1;
System.out.println(nScroll);
for(int i=0;i<m_oChild.getChildCount();i++)
{
oChildView = m_oChild.getChildAt(i);
//-----check if the child is of a certain type
if(oChildView instanceof FishEyeItem)
{
oFishEyeItem = (FishEyeItem)oChildView;
nItemSize = oFishEyeItem.getWidth();
nPositionStart = i * nItemSize;
nPositionEnd = nPositionStart + nItemSize;
oImage = oFishEyeItem.GetImageView();
cFilename = oFishEyeItem.GetImagePath();
//-----check if the item is in visible area
if(oImage!=null)
{
//-----image is in visible area
if(nPositionEnd>=nScroll - (m_nItemBuffer * nItemSize) && nPositionStart - (m_nItemBuffer * nScroll)<=nBoundEnd)
{
//-----check if image needs to be loaded
if(!oFishEyeItem.GetIsImageLoaded())
{
oFishEyeItem.SetIsImageLoaded(true);
new DownloadTaskImage(m_oActivity,
oImage,
cFilename,
nItemSize,
nItemSize,
m_fScaleFactor,
POWERROUNDMODES.ROUND).execute((Void)null);
}
//-----get the item position in the fisheyeview
nItemPosition = nPositionStart - nScroll + (int)(nItemSize*0.5f);
DeterminenSize(oFishEyeItem, Math.abs(m_nReferenceX - nItemPosition), nCurrentClosestDiff);
}
else
{
//-----check if an image can be recycle
if(oFishEyeItem.GetIsImageLoaded())
{
oFishEyeItem.SetIsImageLoaded(false);
new RecycleTaskImage(oImage).execute((Void)null);
}
}
}
}
}
}
}
}
catch(Exception e)
{
Log.d("onScrollChanged", e.toString());
}
}
#Override
public boolean onTouchEvent(MotionEvent oEvent)
{
super.onTouchEvent(oEvent);
try
{
switch(oEvent.getAction())
{
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
break;
default:
break;
}
}
catch(Exception e)
{
Log.d("onTouchEvent", e.toString());
}
return true;
}
protected void onFinishInflate()
{
super.onFinishInflate();
try
{
m_oChild = (LinearLayout)getChildAt(0);
}
catch(Exception e)
{
Log.d("onFinishInflate", e.toString());
}
}
}
Don´t mind some of the unused variables as they are intended to be used later to realize an autoscroll feature once the view itself has finished scrolling (so that the currently closed item will always be in the middle up on letting go of the scrollview).
The View requires to actually be filled with "FishEyeItem"s, which are then used to load images and resize the content. Those Items are loaded during runtime after I have gained the list of items that i need to display.
The FishEyeItem code is the following.
public class FishEyeItem extends RelativeLayout
{
//****************************************************************************************************
// variables
//****************************************************************************************************
//-----determines if this item can be resized
private boolean m_bCanBeResized = false;
//-----path to the image of this fisheye items image
private String m_cImagePath = null;
//-----determines if this item is in range for the fisheye calculation
private boolean m_bIsInRange = true;
//-----determines if the image is loaded already, thus occupying memory
private boolean m_bIsImageLoaded = false;
//-----id of the image4view holding the image
private int m_nImageViewID = -1;
//-----the id of the view in this view which is responsible for resizing
private int m_nResizeViewID = -1;
//****************************************************************************************************
// constructor
//****************************************************************************************************
public FishEyeItem(Context context)
{
super(context);
}
public FishEyeItem(Context context, AttributeSet attrs)
{
super(context, attrs);
}
//****************************************************************************************************
// setter
//****************************************************************************************************
public void SetCanBeResized(boolean bValue)
{
m_bCanBeResized = bValue;
}
public void SetImagePath(String cValue)
{
m_cImagePath = cValue;
}
public void SetIsInRange(boolean bValue)
{
m_bIsInRange = bValue;
}
public void SetIsImageLoaded(boolean bValue)
{
m_bIsImageLoaded = bValue;
}
public void SetImageViewID(int nValue)
{
m_nImageViewID = nValue;
}
public void SetResizeViewID(int nValue)
{
m_nResizeViewID = nValue;
}
//****************************************************************************************************
// getter
//****************************************************************************************************
public boolean GetCanBeResized()
{
return m_bCanBeResized;
}
public String GetImagePath()
{
return m_cImagePath;
}
public boolean GetIsInRange()
{
return m_bIsInRange;
}
public boolean GetIsImageLoaded()
{
return m_bIsImageLoaded;
}
public int GetImageViewID()
{
return m_nImageViewID;
}
public int GetResizeViewID()
{
return m_nResizeViewID;
}
public ImageView GetImageView()
{
ImageView oView = null;
try
{
oView = (ImageView)findViewById(m_nImageViewID);
}
catch(Exception e)
{
Log.d("GetImageView", e.toString());
}
return oView;
}
//****************************************************************************************************
// getter
//****************************************************************************************************
public void Resize(int nWidth, int nHeight)
{
try
{
View oView = findViewById(m_nResizeViewID);
if(oView!=null)
{
System.out.println("Resizing Item" + m_cImagePath);
//-----set the minimum size
RelativeLayout.LayoutParams oParams = (RelativeLayout.LayoutParams)oView.getLayoutParams();
oParams.width = nWidth;
oParams.height = nHeight;
oView.setLayoutParams(oParams);
}
}
catch(Exception e)
{
Log.d("Resize", e.toString());
}
}
}
So essentially everytime the onScrollChanged() is called, the image of an item will be loaded or recycled (both are in running in a async task so they dont block the scrolling and GUI). Also the size of an item will be determined if it is a certain distance away from the middle of the scrollview.
Like I said the Resize() method always gets called (thats why the system.out is there) but when "bouncing" against the borders the items are no longer resized.
So I am guessing the problem is somewhere in the HorizontalScrollView class itself, e.g. a certain flag gets set when "bouncing" against the borders.
EDIT:
So okay I was able to prevent that the items were not able to be updated anymore by simply checking the getScrollX() in the onscrollchanged() and returning if that value is <= 0 or if the value is >= the max bounds. However this still does not explain the fact that the items were not updated anymore when "bouncing" against the borders.
I'm placing ImageView's in a RelativeLayout. I'm setting them with LayoutParams and using setMargins() to set the location of each picture. The max number of Images that will be placed on top of the first one will only reach 8. Their are 5 diffident Images and 8 positions on the screen where they can be placed. I would like to create the Images as their corresponding buttons are pressed and to be able to set that Image into the RelativeLayout and display the change. I would like a way to clear all the Images off the screen except for the main/ background ImageView. I don't like to populate 8 X 5 = 40 Images and then hide them all then change their view to Visible when i need them to show. I need something that will populate as need be but able to destory or remove when I clear it out.
Thanks,
Zelda
aButton.setOnClickListener(new OnClickListener(){
public void onClick(View v)
{
noteNumber++;
if(noteNumber <= 8){
note n = new note(getBaseContext());
n.setNoteNumber(noteNumber);
n.setHeight(85);
images.add(n); //ArrayList()
}
populate();
}
});
}
public void populate(){
//if(noteNumber < 9){
for(note a : images){
//note a = images.get(noteNumber-1); //images is of type ArrayList<ImageView>()
if(a != null && a.getMasterImage() != null){
int number = a.getNoteNumber();
imageParams.setMargins(25+45*number, a.getHeight(), 20, 360);
frame.addView(a.getMasterImage(),imageParams);
}
}
}
}
public class note {
private int noteNumber;
private int height;
private ImageView masterImage;
public note(Context c){
masterImage = new ImageView(c);
masterImage.setImageResource(R.raw.zelda);
this.noteNumber = 1;
height = 0;
}
/**
* #return the masterImage
*/
public ImageView getMasterImage() {
return masterImage;
}
/**
* #param masterImage the masterImage to set
*/
public void setMasterImage(ImageView masterImage) {
this.masterImage = masterImage;
}
/**
* #return the noteNumber
*/
public int getNoteNumber() {
return noteNumber;
}
/**
* #param noteNumber the noteNumber to set
*/
public void setNoteNumber(int noteNumber) {
this.noteNumber = noteNumber;
}
/**
* #return the height
*/
public int getHeight() {
return height;
}
/**
* #param height the height to set
*/
public void setHeight(int height) {
this.height = height;
}
}