/* * Copyright (C) 2011 Daniel Berndt - Codeus Ltd - DateSlider * * This class contains all the scrolling logic of the slidable 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.catglo.widgets; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.Display; import android.view.Gravity; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.Scroller; public class ScrollLayout extends LinearLayout { private static String TAG = "SCROLLLAYOUT"; private Scroller mScroller; private boolean mDragMode; private int mLastX, mLastScroll,mFirstElemOffset,childrenWidth,mScrollX; private VelocityTracker mVelocityTracker; private int mMinimumVelocity, mMaximumVelocity, mInitialOffset; private long currentTime; private int objWidth, objHeight; private DateSlider.Labeler labeler; private OnScrollListener listener; private TimeView mCenterView; 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); } /** * This method is called usually after a ScrollLayout is instanciated, 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(DateSlider.Labeler labeler, long time, int objwidth, int objheight) { this.labeler = labeler; currentTime = time; objWidth = (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-0*objWidth && labeler!=null) { LayoutParams lp = new LayoutParams(objWidth, objHeight); if (childrenWidth==0) { TimeView ttv = labeler.createView(getContext(),true); ttv.setVals(labeler.getElem(currentTime)); addView((View)ttv,lp); mCenterView = ttv; childrenWidth += objWidth; } TimeView ttv = labeler.createView(getContext(),false); ttv.setVals(labeler.add(((TimeView)getChildAt(getChildCount()-1)).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 += objWidth+objWidth; } } @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); 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 * @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 currelem = (TimeView)getChildAt(pos); if (loops>2 || currelem!=null && currelem.getStartTime() <= time && currelem.getEndTime() >= time) { if (loops>2) { Log.d(TAG,String.format("time: %d, start: %d, end: %d", time, currelem.getStartTime(), currelem.getStartTime())); return; } double center = getWidth()/2.0; int left = (getChildCount()/2)*objWidth-getScrollX(); double currper = (center-left)/objWidth; double goalper = (time-currelem.getStartTime())/(double)(currelem.getEndTime()-currelem.getStartTime()); int shift = (int)Math.round((currper-goalper)*objWidth); mScrollX-=shift; reScrollTo(mScrollX,0,false); } else if ( currelem!=null){ double diff = currelem.getEndTime() - currelem.getStartTime(); int steps = (int)Math.round(((time-(currelem.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 * @param y * @param notify if false, the listeners won't be called */ protected void reScrollTo(int x, int y, boolean notify) { if (getChildCount()>0) { mFirstElemOffset += x - mLastScroll; if (mFirstElemOffset - mInitialOffset > objWidth/2) { int stepsRight = (mFirstElemOffset-mInitialOffset+objWidth/2)/objWidth; moveElements(-stepsRight); mFirstElemOffset = ((mFirstElemOffset-mInitialOffset-objWidth/2) % objWidth)+mInitialOffset-objWidth/2; } else if (mInitialOffset - mFirstElemOffset > objWidth/2) { int stepsLeft = (mInitialOffset+objWidth/2-mFirstElemOffset)/objWidth; moveElements(stepsLeft); mFirstElemOffset = (mInitialOffset + objWidth/2 - ((mInitialOffset+objWidth/2-mFirstElemOffset)%objWidth)); } } super.scrollTo(mFirstElemOffset,y); if (listener!=null && notify) { double center = getWidth()/2.0; int left = (getChildCount()/2)*objWidth-mFirstElemOffset; double f = (center-left)/objWidth; long newTime = (long)(mCenterView.getStartTime()+(mCenterView.getEndTime()-mCenterView.getStartTime())*f); listener.onScroll(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 */ protected 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++) { DateSlider.TimeObject newTo = labeler.add(((TimeView)getChildAt(i-1)).getEndTime(),1); ((TimeView)getChildAt(i)).setVals(newTo); } if (getChildCount() + steps <= 0) { for (int i=0;i<getChildCount();i++) { DateSlider.TimeObject newTo = labeler.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--) { DateSlider.TimeObject newTo = labeler.add(((TimeView)getChildAt(i+1)).getEndTime(),-1); ((TimeView)getChildAt(i)).setVals(newTo); } if (steps>=getChildCount()) { for (int i=0;i<getChildCount();i++) { DateSlider.TimeObject newTo = labeler.add(((TimeView)getChildAt(i)).getEndTime(),-steps); ((TimeView)getChildAt(i)).setVals(newTo); } } } } /** * finding whether to scroll or not */ @Override public boolean onTouchEvent(MotionEvent 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); } case MotionEvent.ACTION_CANCEL: default: mDragMode = false; } mLastX = x; return true; } /** * causes the underlying mScroller to do a fling action which will be recovered in the * computeScroll method * @param velocityX */ 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) { listener = l; } public interface OnScrollListener { public void onScroll(long x); } }