Java tutorial
/* * Copyright 2013 Wenhui Yao * * 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.wenhui.syncedListView.lib; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.support.v4.view.GestureDetectorCompat; import android.support.v4.view.MotionEventCompat; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.view.animation.LinearInterpolator; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.OverScroller; import android.widget.Scroller; public class SyncedListLayout extends LinearLayout { private static final String TAG = "SyncedListLayout"; private static final int TOUCH_MODE_FLING = 1; private static final int TOUCH_MODE_SCROLL = 2; private static final int TOUCH_MODE_REST = 0; private static final int DEFAULT_SCROLL_ANIMATION_DURATION = 60 * 1000; // MINUTE private static final int DEFAULT_VELOCITY = 1500; // PER MINUTE private static final long DEFAULT_ANIMATION_DELAY = 10L; private ListView mListViewLeft; private ListView mListViewRight; private GestureDetectorCompat gestureDetector; private int mLastFlingY = 0; private float mRightScrollFactor = 0.8f; private float mLeftScrollFactor = 1.4f; private MotionEvent mDownEvent; private boolean mAnimating = false; private boolean mRequestStopAnim = false; private FlingRunnable mFlingRunnable; private AnimationRunnable mAnimationRunnable; private int mAnimationDuration = DEFAULT_SCROLL_ANIMATION_DURATION; private int mAnimationVelocity = DEFAULT_VELOCITY; private int mLeftListId = 0, mRightListId = 0; private float mLeftAnimationScrollFactor = 2f, mRightAnimationScrollFactor = 1f; private Scroller mScroller; private int mTouchMode = TOUCH_MODE_REST; public SyncedListLayout(Context context) { super(context); init(context, null); } public SyncedListLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } public SyncedListLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } private void init(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SyncedListLayout); try { mLeftListId = a.getResourceId(R.styleable.SyncedListLayout_left_id, 0); mRightListId = a.getResourceId(R.styleable.SyncedListLayout_right_id, 0); mLeftScrollFactor = a.getFloat(R.styleable.SyncedListLayout_left_scroll_factor, 1f); mRightScrollFactor = a.getFloat(R.styleable.SyncedListLayout_right_scroll_factor, 1f); } finally { a.recycle(); } WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics metric = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(metric); mAnimationVelocity = (int) (DEFAULT_VELOCITY * metric.density); gestureDetector = new GestureDetectorCompat(context, gestureListener); mScroller = new Scroller(context); mAnimationRunnable = new AnimationRunnable(context); mFlingRunnable = new FlingRunnable(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stopAnimation(); removeCallbacks(mAnimationRunnable); } @Override protected void onFinishInflate() { super.onFinishInflate(); mListViewLeft = (ListView) findViewById(mLeftListId); mListViewRight = (ListView) findViewById(mRightListId); if (mListViewRight == null || mListViewLeft == null) { throw new IllegalStateException("Either left list or right list cannot be null"); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true; } @Override public boolean onTouchEvent(MotionEvent event) { boolean handle = gestureDetector.onTouchEvent(event); int action = MotionEventCompat.getActionMasked(event); switch (action) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: Log.d(TAG, "touch mode " + mTouchMode); if (mTouchMode != TOUCH_MODE_FLING) { startAnimationInternal(DEFAULT_ANIMATION_DELAY); } mTouchMode = TOUCH_MODE_REST; break; } return handle; } private void dispatchTouchToList(final MotionEvent e) { if (mDownEvent == null) { return; } int leftListWidth = mListViewLeft.getWidth(); if (mDownEvent.getX() <= mListViewLeft.getWidth()) { mListViewLeft.dispatchTouchEvent(mDownEvent); mListViewLeft.dispatchTouchEvent(e); } else { // For some reason, this will only recognize x of left list mDownEvent.offsetLocation(-leftListWidth, 0f); e.offsetLocation(-leftListWidth, 0f); mListViewRight.dispatchTouchEvent(mDownEvent); mListViewRight.dispatchTouchEvent(e); } mDownEvent.recycle(); mDownEvent = null; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { if (mAnimating) { stopAnimationInternal(); } int curY = mScroller.getCurrY(); int distance = curY - mLastFlingY; mLastFlingY = curY; if (distance != 0) { mFlingRunnable.setDistance(distance); post(mFlingRunnable); } if (mScroller.isFinished() || distance == 0) { startAnimationInternal(DEFAULT_ANIMATION_DELAY); } } } public void setLeftListView(ListView left) { this.mListViewLeft = left; } public void setRightListView(ListView right) { this.mListViewRight = right; } public void setLeftAnimationScrollFactor(float factor) { this.mLeftAnimationScrollFactor = factor; } public void setRightAnimationScrollFactor(float factor) { this.mRightAnimationScrollFactor = factor; } public boolean isAnimating() { return mAnimating; } public void startAnimation(long delay) { mRequestStopAnim = false; startAnimationInternal(delay); } private void startAnimationInternal(long delay) { if (mAnimating) { return; } if (mRequestStopAnim) { return; } mAnimating = true; postDelayed(mAnimationLaunchRunnable, delay); } private Runnable mAnimationLaunchRunnable = new Runnable() { @Override public void run() { mAnimationRunnable.startAnimation(mAnimationVelocity, mAnimationDuration); } }; public void stopAnimation() { mRequestStopAnim = true; stopAnimationInternal(); } private void stopAnimationInternal() { mAnimating = false; removeCallbacks(mAnimationLaunchRunnable); mAnimationRunnable.cancel(); removeCallbacks(mAnimationRunnable); } public void setLeftScrollFactor(float factor) { this.mLeftScrollFactor = factor; } public void setRightScrollFactor(float factor) { this.mRightScrollFactor = factor; } /** * * @param velocity Distance per second */ public void setAnimationVelocity(int velocity) { this.mAnimationVelocity = velocity * 60; } private class FlingRunnable implements Runnable { private float distance = 0; public void setDistance(float distance) { this.distance = distance; } @Override public void run() { scrollListBy(distance, mLeftScrollFactor, mRightScrollFactor); } } private void scrollListBy(float distance, float leftScrollFactor, float rightScrollFactor) { scrollListBy(mListViewRight, (int) (distance * rightScrollFactor + 0.5f)); scrollListBy(mListViewLeft, (int) (distance * leftScrollFactor + 0.5f)); } private void scrollListBy(ListView target, int deltaY) { final int firstPosition = target.getFirstVisiblePosition(); if (firstPosition == ListView.INVALID_POSITION) { return; } final View firstView = target.getChildAt(0); if (firstView == null) { return; } final int newTop = firstView.getTop() - deltaY; target.setSelectionFromTop(firstPosition, newTop); } private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { mScroller.forceFinished(true); if (mDownEvent != null) { mDownEvent.recycle(); } mDownEvent = MotionEvent.obtain(e); removeCallbacks(mFlingRunnable); stopAnimationInternal(); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mTouchMode = TOUCH_MODE_FLING; mLastFlingY = mListViewRight.getScrollY(); mScroller.fling(0, mLastFlingY, 0, (int) -velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { mTouchMode = TOUCH_MODE_SCROLL; mFlingRunnable.setDistance(distanceY); post(mFlingRunnable); return true; } @Override public boolean onSingleTapUp(MotionEvent e) { dispatchTouchToList(e); return false; } }; private class AnimationRunnable implements Runnable { private int lastY; private final OverScroller scroller; private int distance, duration; private boolean cancelled = false; public AnimationRunnable(Context context) { scroller = new OverScroller(context, new LinearInterpolator()); } public void startAnimation(int distance, int duration) { cancelled = false; this.distance = distance; this.duration = duration; lastY = mListViewRight.getScrollY(); animate(); } private void animate() { scroller.startScroll(0, lastY, 0, distance, duration); post(this); } public void cancel() { cancelled = true; scroller.forceFinished(true); } @Override public void run() { if (cancelled) { return; } boolean hasMore = scroller.computeScrollOffset(); int y = scroller.getCurrY(); int yDiff = y - lastY; if (yDiff != 0) { scrollListBy(yDiff, mLeftAnimationScrollFactor, mRightAnimationScrollFactor); lastY = y; } if (hasMore) { post(this); } else { animate(); } } } }