com.apptentive.android.sdk.module.messagecenter.view.MessageCenterListView.java Source code

Java tutorial

Introduction

Here is the source code for com.apptentive.android.sdk.module.messagecenter.view.MessageCenterListView.java

Source

/*
 * Copyright (c) 2015, Apptentive, Inc. All Rights Reserved.
 * Please refer to the LICENSE file for the terms and conditions
 * under which redistribution and use of this file is permitted.
 */
package com.apptentive.android.sdk.module.messagecenter.view;

import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.os.Parcelable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;

import android.widget.HeaderViewListAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;

import com.apptentive.android.sdk.R;

public class MessageCenterListView extends ListView {

    public interface ApptentiveMessageCenterListAdapter extends ListAdapter {
        /**
         * True if views of given type will be sticky at the top
         */
        boolean isItemSticky(int viewType);
    }

    /**
     * Wrapper class for sticky view and its position in the list.
     */
    static class StickyWrapper {
        public View view;
        public int position;
        public long id;
        public int additionalIndent;
    }

    private final Rect touchRect = new Rect();
    private final PointF touchPt = new PointF();
    private int touchSlop;
    private View touchTarget;
    private MotionEvent downEvent;

    // fields used for drawing shadow under the sticky header
    private GradientDrawable shadowDrawable;
    private int shadowHeight;

    // Optional delegating listener
    OnScrollListener delegateScrollListener;

    // shadow for being recycled
    StickyWrapper recycledHeaderView;

    /**
     * shadow instance with a sticky view, can be null.
     */
    StickyWrapper stickyWrapper;

    private OnListviewResizeListener resizeListener;
    /**
     * Scroll listener
     */
    private final OnScrollListener mOnScrollListener = new OnScrollListener() {

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if (delegateScrollListener != null) {
                delegateScrollListener.onScrollStateChanged(view, scrollState);
            }
        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

            if (delegateScrollListener != null) {
                delegateScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
            }

            ListAdapter adapter = getAdapter();
            if (adapter == null || visibleItemCount == 0)
                return; // nothing to do

            final boolean isFirstVisibleItemHeader = isItemSticky(adapter,
                    adapter.getItemViewType(firstVisibleItem));

            if (isFirstVisibleItemHeader) {
                View headerView = getChildAt(0);
                int headerTop = headerView.getTop();
                int pad = getPaddingTop();
                if (headerTop == pad) {
                    // view sticks to the top, do not render shadow
                    destroyStickyShadow();
                } else {
                    tryCreateShadowAtPosition(firstVisibleItem, firstVisibleItem, visibleItemCount);
                }

            } else {
                // header is not at the first visible position
                int headerPosition = findCurrentHeaderPosition(firstVisibleItem);
                if (headerPosition > -1) {
                    tryCreateShadowAtPosition(headerPosition, firstVisibleItem, visibleItemCount);
                } else { // there is no section for the first visible item, destroy shadow
                    destroyStickyShadow();
                }
            }
        }

    };

    /**
     * Default change observer.
     */
    private final DataSetObserver dataSetObserver = new DataSetObserver() {
        @Override
        public void onChanged() {
            recreateStickyShadow();
        }

        @Override
        public void onInvalidated() {
            recreateStickyShadow();
        }
    };

    public interface OnListviewResizeListener {
        void OnListViewResize(int w, int h, int oldw, int oldh);
    }

    public void setOnListViewResizeListener(OnListviewResizeListener l) {
        resizeListener = l;
    }

    public MessageCenterListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public MessageCenterListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    private void initView() {
        setOnScrollListener(mOnScrollListener);
        touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        initShadow(true);
    }

    public void initShadow(boolean visible) {
        if (visible) {
            if (shadowDrawable == null) {
                shadowDrawable = (GradientDrawable) ContextCompat.getDrawable(getContext(),
                        R.drawable.apptentive_listview_item_shadow);
                shadowHeight = (int) (4 * getResources().getDisplayMetrics().density);
            }
        } else {
            if (shadowDrawable != null) {
                shadowDrawable = null;
                shadowHeight = 0;
            }
        }
    }

    /**
     * Create shadow wrapper with a sticky view  at given position
     */
    void createStickyShadow(int position) {

        // recycle shadow
        StickyWrapper stickyViewShadow = recycledHeaderView;
        recycledHeaderView = null;

        // create new shadow, if needed
        if (stickyViewShadow == null) {
            stickyViewShadow = new StickyWrapper();
        }
        // request new view using recycled view, if such
        View stickyView = getAdapter().getView(position, stickyViewShadow.view, MessageCenterListView.this);

        // read layout parameters
        LayoutParams layoutParams = (LayoutParams) stickyView.getLayoutParams();
        if (layoutParams == null) {
            layoutParams = (LayoutParams) generateDefaultLayoutParams();
            stickyView.setLayoutParams(layoutParams);
        }

        View childLayout = ((ViewGroup) stickyView).getChildAt(0);
        int heightMode = MeasureSpec.getMode(layoutParams.height);
        int heightSize = MeasureSpec.getSize(layoutParams.height);

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightMode = MeasureSpec.EXACTLY;
        }

        int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom();
        if (heightSize > maxHeight) {
            heightSize = maxHeight;
        }
        // assuming left and right additional paddings are the same
        int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(),
                MeasureSpec.EXACTLY);
        int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
        stickyView.measure(ws, hs);
        stickyView.layout(0, 0, stickyView.getMeasuredWidth(), stickyView.getMeasuredHeight());

        // initialize shadow
        stickyViewShadow.view = stickyView;
        stickyViewShadow.position = position;
        stickyViewShadow.id = getAdapter().getItemId(position);
        stickyViewShadow.additionalIndent = childLayout.getPaddingLeft();

        stickyWrapper = stickyViewShadow;
    }

    /**
     * Destroy shadow wrapper for current sticky view
     */
    void destroyStickyShadow() {
        if (stickyWrapper != null) {
            // keep shadow for being recycled later
            recycledHeaderView = stickyWrapper;
            stickyWrapper = null;
        }
    }

    /**
     * Create sticky shadowded view at a given item position.
     */
    void tryCreateShadowAtPosition(int headerPosition, int firstVisibleItem, int visibleItemCount) {
        if (visibleItemCount < 1) {
            // no need for creating shadow if no visible item
            destroyStickyShadow();
            return;
        }

        if (stickyWrapper != null && stickyWrapper.position != headerPosition) {
            // invalidate shadow, if required
            destroyStickyShadow();
        }

        if (stickyWrapper == null) {
            createStickyShadow(headerPosition);
        }

    }

    int findCurrentHeaderPosition(int fromPosition) {
        ListAdapter adapter = getAdapter();

        if (fromPosition >= adapter.getCount())
            return -1; // dataset has changed, no candidate

        // Only need to look through to the next section item above
        for (int position = fromPosition; position >= 0; position--) {
            int viewType = adapter.getItemViewType(position);
            if (isItemSticky(adapter, viewType))
                return position;
        }
        return -1;
    }

    void recreateStickyShadow() {
        destroyStickyShadow();
        ListAdapter adapter = getAdapter();
        if (adapter != null && adapter.getCount() > 0) {
            int firstVisiblePosition = getFirstVisiblePosition();
            int headerPosition = findCurrentHeaderPosition(firstVisiblePosition);
            if (headerPosition == -1) {
                return;
            }
            tryCreateShadowAtPosition(headerPosition, firstVisiblePosition,
                    getLastVisiblePosition() - firstVisiblePosition + 1);
        }
    }

    @Override
    public void setOnScrollListener(OnScrollListener listener) {
        if (listener == mOnScrollListener) {
            super.setOnScrollListener(listener);
        } else {
            delegateScrollListener = listener;
        }
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        post(new Runnable() {
            @Override
            public void run() {
                // restore view after configuration change
                recreateStickyShadow();
            }
        });
    }

    @Override
    public void setAdapter(ListAdapter adapter) {
        // unregister observer at old adapter and register on new one
        ListAdapter oldAdapter = getAdapter();
        if (oldAdapter != null) {
            oldAdapter.unregisterDataSetObserver(dataSetObserver);
        }
        if (adapter != null) {
            adapter.registerDataSetObserver(dataSetObserver);
        }

        if (oldAdapter != adapter) {
            destroyStickyShadow();
        }

        super.setAdapter(adapter);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (stickyWrapper != null) {
            int parentWidth = r - l - getPaddingLeft() - getPaddingRight();
            int shadowWidth = stickyWrapper.view.getWidth();
            if (parentWidth != shadowWidth) {
                recreateStickyShadow();
            }
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (resizeListener != null) {
            resizeListener.OnListViewResize(w, h, oldw, oldh);
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        if (stickyWrapper != null) {

            int pLeft = getListPaddingLeft();
            int pTop = getListPaddingTop();
            View view = stickyWrapper.view;
            int headerTop = view.getTop();
            pLeft += stickyWrapper.additionalIndent;
            // draw child
            canvas.save();

            int clipHeight = view.getHeight() + (shadowDrawable == null ? 0 : shadowHeight);
            canvas.clipRect(pLeft, pTop, pLeft + view.getWidth() - 2 * stickyWrapper.additionalIndent,
                    pTop + clipHeight);

            canvas.translate(pLeft - stickyWrapper.additionalIndent, pTop - headerTop);
            drawChild(canvas, stickyWrapper.view, getDrawingTime());

            if (shadowDrawable != null) {
                shadowDrawable.setBounds(stickyWrapper.view.getLeft(), stickyWrapper.view.getBottom(),
                        stickyWrapper.view.getRight(), stickyWrapper.view.getBottom() + shadowHeight);
                shadowDrawable.draw(canvas);
            }

            canvas.restore();
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        final float x = ev.getX();
        final float y = ev.getY();
        final int action = ev.getAction();

        if (action == MotionEvent.ACTION_DOWN && touchTarget == null && stickyWrapper != null
                && isStickyViewTouched(stickyWrapper.view, x, y)) {
            touchTarget = stickyWrapper.view;
            touchPt.x = x;
            touchPt.y = y;

            downEvent = MotionEvent.obtain(ev);
        }

        if (touchTarget != null) {
            if (isStickyViewTouched(touchTarget, x, y)) {
                // forward event to header view
                touchTarget.dispatchTouchEvent(ev);
            }

            if (action == MotionEvent.ACTION_UP) {
                super.dispatchTouchEvent(ev);
                clearTouchTarget();

            } else if (action == MotionEvent.ACTION_CANCEL) {
                clearTouchTarget();

            } else if (action == MotionEvent.ACTION_MOVE) {
                if (Math.abs(y - touchPt.y) > touchSlop) {

                    MotionEvent event = MotionEvent.obtain(ev);
                    event.setAction(MotionEvent.ACTION_CANCEL);
                    touchTarget.dispatchTouchEvent(event);
                    event.recycle();

                    super.dispatchTouchEvent(downEvent);
                    super.dispatchTouchEvent(ev);
                    clearTouchTarget();

                }
            }

            return true;
        }

        return super.dispatchTouchEvent(ev);
    }

    private boolean isStickyViewTouched(View view, float x, float y) {
        view.getHitRect(touchRect);

        touchRect.bottom += getPaddingTop() - view.getTop();
        touchRect.left += getPaddingLeft();
        touchRect.right -= getPaddingRight();
        return touchRect.contains((int) x, (int) y);
    }

    private void clearTouchTarget() {
        touchTarget = null;
        if (downEvent != null) {
            downEvent.recycle();
            downEvent = null;
        }
    }

    public static boolean isItemSticky(ListAdapter adapter, int viewType) {
        if (adapter instanceof HeaderViewListAdapter) {
            adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
        }
        return ((ApptentiveMessageCenterListAdapter) adapter).isItemSticky(viewType);
    }

}