Android Open Source - LearnByHeart Foldable List Layout






From Project

Back to project page LearnByHeart.

License

The source code is released under:

Apache License

If you think the Android project LearnByHeart listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.mps.learn.pb.ui.views;
//from  w w  w.j ava 2s.  c  o  m
import java.util.LinkedList;
import java.util.Queue;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;

import com.mps.learn.pb.ui.views.shading.FoldShading;
import com.mps.learn.pb.ui.views.shading.SimpleFoldShading;

/**
 * Foldable items list layout.
 * <p/>
 * It wraps views created by given BaseAdapter into FoldableItemLayouts and provides functionality to scroll
 * among them.
 */
public class FoldableListLayout extends FrameLayout implements GestureDetector.OnGestureListener {

    private static final long ANIMATION_DURATION_PER_ITEM = 600;

    private static final LayoutParams PARAMS = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    private static final int CACHED_LAYOUTS_OFFSET = 2;

    private OnFoldRotationListener mFoldRotationListener;
    private BaseAdapter mAdapter;

    private float mFoldRotation;
    private float mMinRotation, mMaxRotation;

    private FoldableItemLayout mFirstLayout, mSecondLayout;
    private FoldShading mFoldShading;

    private SparseArray<FoldableItemLayout> mFoldableLayoutsMap = new SparseArray<FoldableItemLayout>();
    private Queue<FoldableItemLayout> mFoldableLayoutsCache = new LinkedList<FoldableItemLayout>();

    private ObjectAnimator mAnimator;
    private long mLastEventTime;
    private boolean mLastEventResult;
    private GestureDetector mGestureDetector;

    private float mMinDistanceBeforeScroll;
    private boolean mIsScrollDetected;
    private float mScrollStartRotation;
    private float mScrollStartDistance;

    public FoldableListLayout(Context context) {
        super(context);
        init(context);
    }

    public FoldableListLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public FoldableListLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    private void init(Context context) {
        mGestureDetector = new GestureDetector(context, this);
        mAnimator = ObjectAnimator.ofFloat(this, "foldRotation", 0);
        mMinDistanceBeforeScroll = ViewConfiguration.get(context).getScaledPagingTouchSlop();

        mFoldShading = new SimpleFoldShading();
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        // We want manually draw only selected children
        if (mFirstLayout != null) mFirstLayout.draw(canvas);
        if (mSecondLayout != null) mSecondLayout.draw(canvas);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        super.dispatchTouchEvent(ev);
        return getCount() > 0; // No touches for underlying views if we have items
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // Listening for events but propogates them to children if no own gestures are detected
        return processTouch(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // We will be here if no children wants to handle current touches or if own gesture is detected
        return processTouch(event);
    }

    public void setOnFoldRotationListener(OnFoldRotationListener listener) {
        mFoldRotationListener = listener;
    }

    /**
     * Setting shading to use during fold rotation. Should be called before {@link #setAdapter(android.widget.BaseAdapter)}
     */
    public void setFoldShading(FoldShading shading) {
        mFoldShading = shading;
    }

    public void setAdapter(BaseAdapter adapter) {
        if (mAdapter != null) mAdapter.unregisterDataSetObserver(mDataObserver);
        mAdapter = adapter;
        if (mAdapter != null) mAdapter.registerDataSetObserver(mDataObserver);
        updateAdapterData();
    }

    public BaseAdapter getAdapter() {
        return mAdapter;
    }

    public int getCount() {
        return mAdapter == null ? 0 : mAdapter.getCount();
    }

    private void updateAdapterData() {
        int size = getCount();
        mMinRotation = 0;
        mMaxRotation = size == 0 ? 0 : 180 * (size - 1);

        freeAllLayouts(); // clearing old bindings

        // recalculating items
        setFoldRotation(mFoldRotation);
    }

    public final void setFoldRotation(float rotation) {
        setFoldRotation(rotation, false);
    }

    protected void setFoldRotation(float rotation, boolean isFromUser) {
        if (isFromUser) mAnimator.cancel();

        rotation = Math.min(Math.max(mMinRotation, rotation), mMaxRotation);

        mFoldRotation = rotation;

        int firstVisiblePosition = (int) (rotation / 180);
        float localRotation = rotation % 180;

        int size = getCount();
        boolean isHasFirst = firstVisiblePosition < size;
        boolean isHasSecond = firstVisiblePosition + 1 < size;

        FoldableItemLayout firstLayout = isHasFirst ? getLayoutForItem(firstVisiblePosition) : null;
        FoldableItemLayout secondLayout = isHasSecond ? getLayoutForItem(firstVisiblePosition + 1) : null;

        if (isHasFirst) {
            firstLayout.setFoldRotation(localRotation);
            onFoldRotationChanged(firstLayout, firstVisiblePosition);
        }

        if (isHasSecond) {
            secondLayout.setFoldRotation(localRotation - 180);
            onFoldRotationChanged(secondLayout, firstVisiblePosition + 1);
        }

        boolean isReversedOrder = localRotation <= 90;

        if (isReversedOrder) {
            mFirstLayout = secondLayout;
            mSecondLayout = firstLayout;
        } else {
            mFirstLayout = firstLayout;
            mSecondLayout = secondLayout;
        }

        if (mFoldRotationListener != null) mFoldRotationListener.onFoldRotation(rotation, isFromUser);

        invalidate(); // when hardware acceleration is enabled view may not be invalidated and redrawn, but we need it
    }

    protected void onFoldRotationChanged(FoldableItemLayout layout, int position) {
        // Subclasses can apply their transformations here
    }

    public float getFoldRotation() {
        return mFoldRotation;
    }

    private FoldableItemLayout getLayoutForItem(int position) {
        FoldableItemLayout layout = mFoldableLayoutsMap.get(position);
        if (layout != null) return layout; // we already have bound layout

        // trying to find cached layout
        layout = mFoldableLayoutsCache.poll();

        if (layout == null) {
            // trying to free used layout (far enough from currently requested)
            int farthestItem = position;

            int size = mFoldableLayoutsMap.size();
            for (int i = 0; i < size; i++) {
                int pos = mFoldableLayoutsMap.keyAt(i);
                if (Math.abs(position - pos) > Math.abs(position - farthestItem)) {
                    farthestItem = pos;
                }
            }

            if (Math.abs(farthestItem - position) > CACHED_LAYOUTS_OFFSET) {
                layout = mFoldableLayoutsMap.get(farthestItem);
                mFoldableLayoutsMap.remove(farthestItem);
                layout.getBaseLayout().removeAllViews(); // clearing old data
            }
        }

        if (layout == null) {
            // if still no suited layout - create it
            layout = new FoldableItemLayout(getContext());
            layout.setFoldShading(mFoldShading);
            addView(layout, PARAMS);
        }

        // binding layout to new data
        View view = mAdapter.getView(position, null, layout.getBaseLayout()); // TODO: use recycler
        layout.getBaseLayout().addView(view, PARAMS);

        mFoldableLayoutsMap.put(position, layout);

        return layout;
    }

    private void freeAllLayouts() {
        int size = mFoldableLayoutsMap.size();
        for (int i = 0; i < size; i++) {
            FoldableItemLayout layout = mFoldableLayoutsMap.valueAt(i);
            layout.getBaseLayout().removeAllViews();
            mFoldableLayoutsCache.offer(layout);
        }
        mFoldableLayoutsMap.clear();
    }

    public void scrollToPosition(int index) {
        index = Math.max(0, Math.min(index, getCount() - 1));

        float rotation = index * 180f;
        float current = getFoldRotation();
        long duration = (long) Math.abs(ANIMATION_DURATION_PER_ITEM * (rotation - current) / 180f);

        mAnimator.cancel();
        mAnimator.setFloatValues(current, rotation);
        mAnimator.setDuration(duration).start();
    }

    protected void scrollToNearestPosition() {
        float current = getFoldRotation();
        scrollToPosition((int) ((current + 90f) / 180f));
    }

    private boolean processTouch(MotionEvent event) {
        // Checking if that event was already processed (by onInterceptTouchEvent prior to onTouchEvent)
        long eventTime = event.getEventTime();
        if (mLastEventTime == eventTime) return mLastEventResult;
        mLastEventTime = eventTime;

        if (event.getActionMasked() == MotionEvent.ACTION_UP && mIsScrollDetected) {
            mIsScrollDetected = false;
            scrollToNearestPosition();
        }

        if (getCount() > 0) {
            // Fixing event's Y position due to performed translation
            MotionEvent eventCopy = MotionEvent.obtain(event);
            eventCopy.offsetLocation(0, getTranslationY());
            mLastEventResult = mGestureDetector.onTouchEvent(eventCopy);
            eventCopy.recycle();
        } else {
            mLastEventResult = false;
        }

        return mLastEventResult;
    }

    @Override
    public boolean onDown(MotionEvent event) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent event) {
        // NO-OP
    }

    @Override
    public boolean onSingleTapUp(MotionEvent event) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent event) {
        // NO-OP
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        float distance = e1.getY() - e2.getY();

        if (!mIsScrollDetected && Math.abs(distance) > mMinDistanceBeforeScroll) {
            mIsScrollDetected = true;
            mScrollStartRotation = getFoldRotation();
            mScrollStartDistance = distance;
        }

        if (mIsScrollDetected) {
            float rotation = (2 * (distance - mScrollStartDistance) / getHeight()) * 180f;
            setFoldRotation(mScrollStartRotation + rotation, true);
        }

        return mIsScrollDetected;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        float rotation = getFoldRotation();
        if (rotation % 180 == 0) return false;

        int position = (int) (rotation / 180f);
        scrollToPosition(velocityY > 0 ? position : position + 1);
        return true;
    }

    private DataSetObserver mDataObserver = new DataSetObserver() {
        @Override
        public void onChanged() {
            super.onChanged();
            updateAdapterData();
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            updateAdapterData();
        }
    };

    public interface OnFoldRotationListener {
        void onFoldRotation(float rotation, boolean isFromUser);
    }

}




Java Source Code List

com.mps.learn.pb.App.java
com.mps.learn.pb.PhraseManager.java
com.mps.learn.pb.adapter.AllPhraseAdapter.java
com.mps.learn.pb.alarm.ReminderReceiver.java
com.mps.learn.pb.alarm.ReminderUtil.java
com.mps.learn.pb.db.DataSourcePhrase.java
com.mps.learn.pb.db.DatabaseHelper.java
com.mps.learn.pb.db.PhraseReaderContract.java
com.mps.learn.pb.gcm.GcmBroadcastReceiver.java
com.mps.learn.pb.gcm.GcmIntentService.java
com.mps.learn.pb.http.DataUpdaterCallback.java
com.mps.learn.pb.http.DataUpdaterTask.java
com.mps.learn.pb.http.HttpConstant.java
com.mps.learn.pb.http.PhraseParser.java
com.mps.learn.pb.model.PhraseBook.java
com.mps.learn.pb.model.PhraseModel.java
com.mps.learn.pb.ui.activities.AddPhraseActivity.java
com.mps.learn.pb.ui.activities.MainActivity.java
com.mps.learn.pb.ui.activities.PhraseViewActivity.java
com.mps.learn.pb.ui.activities.SettingActivity.java
com.mps.learn.pb.ui.factivities.BaseActivity.java
com.mps.learn.pb.ui.factivities.FActivity.java
com.mps.learn.pb.ui.factivities.FoldableListActivity.java
com.mps.learn.pb.ui.factivities.ItemsAdapter.java
com.mps.learn.pb.ui.factivities.PaintingsAdapter.java
com.mps.learn.pb.ui.factivities.SpannableBuilder.java
com.mps.learn.pb.ui.factivities.UnfoldableDetailsActivity.java
com.mps.learn.pb.ui.views.FoldableItemLayout.java
com.mps.learn.pb.ui.views.FoldableListLayout.java
com.mps.learn.pb.ui.views.UnfoldableView.java
com.mps.learn.pb.ui.views.shading.FoldShading.java
com.mps.learn.pb.ui.views.shading.GlanceFoldShading.java
com.mps.learn.pb.ui.views.shading.SimpleFoldShading.java
com.mps.learn.pb.util.CommonConstants.java
com.mps.learn.pb.util.Debugger.java