/*
* Copyright (C) 2011 Daniel Berndt - Codeus Ltd - DateSlider
*
* This class contains all the scrolling logic of the slide-able elements
*
* 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.codeslap.dateslider;
import com.rareventure.gps2.GTG;
import com.rareventure.gps2.reviewer.EnterFromDateToToDateActivity;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.*;
import android.widget.LinearLayout;
import android.widget.Scroller;
public class ScrollLayout extends LinearLayout {
private static final String TAG = "SCROLLLAYOUT";
private final Scroller mScroller;
private boolean mDragMode;
private int mLastX, mLastScroll, mFirstElemOffset, childrenWidth, mScrollX;
private VelocityTracker mVelocityTracker;
private final int mMinimumVelocity;
private final int mMaximumVelocity;
private int mInitialOffset;
private long currentTime;
private int mObjWidth;
private EnterFromDateToToDateActivity.Labeler mLabeler;
private OnScrollListener mListener;
private TimeView mCenterView;
private long maxTime;
private long minTime;
public ScrollLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(getContext());
setGravity(Gravity.CENTER_VERTICAL);
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
// as mMaximumVelocity does not exist in API<4
float density = getContext().getResources().getDisplayMetrics().density;
mMaximumVelocity = (int) (4000 * 0.5f * density);
}
public void setMinTimeAndMaxTime(long minTime, long maxTime)
{
this.minTime = minTime;
this.maxTime = maxTime;
}
public void setMinTime(long minTime)
{
this.minTime = minTime;
}
public void setMaxTime(long maxTime)
{
this.maxTime = maxTime;
}
/**
* This method is called usually after a ScrollLayout is instantiated, it provides the scroller
* with all necessary information
*
* @param labeler the labeler instance which will provide the ScrollLayout with time
* unit information
* @param time the start time as timestamp representation
* @param objWidth the width of an TimeTextView in dps
* @param objHeight the height of an TimeTextView in dps
*/
public void setLabeler(EnterFromDateToToDateActivity.Labeler labeler, long time, int objWidth, int objHeight) {
this.mLabeler = labeler;
currentTime = time;
mObjWidth = (int) (objWidth * getContext().getResources().getDisplayMetrics().density);
objHeight = (int) (objHeight * getContext().getResources().getDisplayMetrics().density);
// TODO: make it not dependent on the display width but rather on the layout width
Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
int displayWidth = display.getWidth();
while (displayWidth > childrenWidth && labeler != null) {
LayoutParams lp = new LayoutParams(mObjWidth, objHeight);
if (childrenWidth == 0) {
TimeView ttv = labeler.createView(getContext(), true);
ttv.setVals(labeler.getElem(currentTime));
addView((View) ttv, lp);
mCenterView = ttv;
childrenWidth += mObjWidth;
}
TimeView ttv = labeler.createView(getContext(), false);
TimeView lastChild = (TimeView) getChildAt(getChildCount() - 1);
ttv.setVals(labeler.add(lastChild.getEndTime(), 1));
addView((View) ttv, lp);
ttv = labeler.createView(getContext(), false);
ttv.setVals(labeler.add(((TimeView) getChildAt(0)).getEndTime(), -1));
addView((View) ttv, 0, lp);
childrenWidth += mObjWidth + mObjWidth;
}
}
@Override
public void onSizeChanged(int w, int h, int oldWidth, int oldHeight) {
super.onSizeChanged(w, h, oldWidth, oldHeight);
mInitialOffset = (childrenWidth - w) / 2;
super.scrollTo(mInitialOffset, 0);
mScrollX = mInitialOffset;
setTime(currentTime, 0);
}
/**
* this element will position the TimeTextViews such that they correspond to the given time
*
* @param time the time in milliseconds
* @param loops prevents setTime getting called too often, if loop is > 2 the procedure will be
* stopped
*/
public void setTime(long time, int loops) {
currentTime = time;
if (!mScroller.isFinished()) mScroller.abortAnimation();
int pos = getChildCount() / 2;
TimeView currentElement = (TimeView) getChildAt(pos);
if (loops > 2 || currentElement.getStartTime() <= time && currentElement.getEndTime() >= time) {
if (loops > 2) {
// Log.d(TAG, String.format("time: %d, start: %d, end: %d", time, currentElement.getStartTime(), currentElement.getStartTime()));
return;
}
double center = getWidth() / 2.0;
int left = (getChildCount() / 2) * mObjWidth - getScrollX();
double currper = (center - left) / mObjWidth;
double goalper = (time - currentElement.getStartTime()) / (double) (currentElement.getEndTime() - currentElement.getStartTime());
int shift = (int) Math.round((currper - goalper) * mObjWidth);
mScrollX -= shift;
reScrollTo(mScrollX, 0, false);
} else {
double diff = currentElement.getEndTime() - currentElement.getStartTime();
int steps = (int) Math.round(((time - (currentElement.getStartTime() + diff / 2)) / diff));
moveElements(-steps);
setTime(time, loops + 1);
}
}
/**
* scroll the element when the mScroller is still scrolling
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
mScrollX = mScroller.getCurrX();
reScrollTo(mScrollX, 0, true);
// Keep on drawing until the animation has finished.
postInvalidate();
}
}
@Override
public void scrollTo(int x, int y) {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
reScrollTo(x, y, true);
}
/**
* core scroll function which will replace and move TimeTextViews so that they don't get
* scrolled out of the layout
*
* @param x the x scroll
* @param y the y scroll
* @param notify if false, the listeners won't be called
*/
void reScrollTo(int x, int y, boolean notify) {
if (getChildCount() > 0) {
mFirstElemOffset += x - mLastScroll;
double center = getWidth() / 2.0;
int left = (getChildCount() / 2) * mObjWidth - mFirstElemOffset;
double f = (center - left) / mObjWidth;
long newTime = (long) (mCenterView.getStartTime() + (mCenterView.getEndTime()
- mCenterView.getStartTime()) * f);
if(newTime > maxTime)
{
newTime = maxTime;
// left = (getChildCount() / 2) * mObjWidth - mFirstElemOffset;
// f = (center - left) / mObjWidth;
//
// f = (center - (getChildCount() / 2) * mObjWidth + mFirstElemOffset) / mObjWidth
//
// f * mObjWidth = (center - (getChildCount() / 2) * mObjWidth + mFirstElemOffset)
//
// f * mObjWidth - center + (getChildCount() / 2) * mObjWidth = mFirstElemOffset
mFirstElemOffset = (int)(((double)newTime - mCenterView.getStartTime()) / (mCenterView.getEndTime()
- mCenterView.getStartTime()) * mObjWidth - center + (getChildCount() / 2) * mObjWidth);
}
else if(newTime < minTime)
{
newTime = minTime;
mFirstElemOffset = (int)(((double)newTime - mCenterView.getStartTime()) / (mCenterView.getEndTime()
- mCenterView.getStartTime()) * mObjWidth - center + (getChildCount() / 2) * mObjWidth);
}
if (mFirstElemOffset - mInitialOffset > mObjWidth / 2) {
int stepsRight = (mFirstElemOffset - mInitialOffset + mObjWidth / 2) / mObjWidth;
moveElements(-stepsRight);
mFirstElemOffset = ((mFirstElemOffset - mInitialOffset - mObjWidth / 2) % mObjWidth)
+ mInitialOffset - mObjWidth / 2;
} else if (mInitialOffset - mFirstElemOffset > mObjWidth / 2) {
int stepsLeft = (mInitialOffset + mObjWidth / 2 - mFirstElemOffset) / mObjWidth;
moveElements(stepsLeft);
mFirstElemOffset = (mInitialOffset + mObjWidth / 2 - ((mInitialOffset + mObjWidth / 2
- mFirstElemOffset) % mObjWidth));
}
super.scrollTo(mFirstElemOffset, y);
if (mListener != null && notify) {
mListener.onScroll(this, newTime);
}
mLastScroll = x;
}
}
/**
* when the scrolling procedure causes "steps" elements to fall out of the visible layout,
* all TimeTextViews swap their contents so that it appears that there happens an endless
* scrolling with a very limited amount of views
*
* @param steps the amount of steps to move
*/
void moveElements(int steps) {
if (steps < 0) {
for (int i = 0; i < getChildCount() + steps; i++) {
((TimeView) getChildAt(i)).setVals((TimeView) getChildAt(i - steps));
}
for (int i = getChildCount() + steps; i > 0 && i < getChildCount(); i++) {
EnterFromDateToToDateActivity.TimeObject newTo = mLabeler.add(((TimeView) getChildAt(i - 1)).getEndTime(), 1);
((TimeView) getChildAt(i)).setVals(newTo);
}
if (getChildCount() + steps <= 0) {
for (int i = 0; i < getChildCount(); i++) {
EnterFromDateToToDateActivity.TimeObject newTo = mLabeler.add(((TimeView) getChildAt(i)).getEndTime(), -steps);
((TimeView) getChildAt(i)).setVals(newTo);
}
}
} else if (steps > 0) {
for (int i = getChildCount() - 1; i >= steps; i--) {
((TimeView) getChildAt(i)).setVals((TimeView) getChildAt(i - steps));
}
for (int i = steps - 1; i >= 0 && i < getChildCount() - 1; i--) {
EnterFromDateToToDateActivity.TimeObject newTo = mLabeler.add(((TimeView) getChildAt(i + 1)).getEndTime(), -1);
((TimeView) getChildAt(i)).setVals(newTo);
}
if (steps >= getChildCount()) {
for (int i = 0; i < getChildCount(); i++) {
EnterFromDateToToDateActivity.TimeObject newTo = mLabeler.add(((TimeView) getChildAt(i)).getEndTime(), -steps);
((TimeView) getChildAt(i)).setVals(newTo);
}
}
}
}
/**
* finding whether to scroll or not
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Log.d(GTG.TAG,"event is "+ev);
final int action = ev.getAction();
final int x = (int) ev.getX();
if (action == MotionEvent.ACTION_DOWN) {
mDragMode = true;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
}
if (!mDragMode)
return super.onTouchEvent(ev);
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
mScrollX += mLastX - x;
reScrollTo(mScrollX, 0, true);
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000);
int initialVelocity = (int) Math.min(velocityTracker.getXVelocity(), mMaximumVelocity);
if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) {
fling(-initialVelocity);
}
mDragMode = false;
// case MotionEvent.ACTION_CANCEL:
// default:
// mDragMode = false;
default:
return false;
}
mLastX = x;
return true;
}
/**
* causes the underlying mScroller to do a fling action which will be recovered in the
* computeScroll method
*
* @param velocityX the speed of the fling
*/
private void fling(int velocityX) {
if (getChildCount() > 0) {
mScroller.fling(mScrollX, 0, velocityX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
invalidate();
}
}
public void setOnScrollListener(OnScrollListener l) {
mListener = l;
}
public interface OnScrollListener {
public void onScroll(ScrollLayout source, long x);
}
}