public class SlideStackView extends ViewGroup{ public SlideStackView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); ... initAttributes(context, attrs); // we need to draw childs in reverse order setChildrenDrawingOrderEnabled(true); } public SlideStackView(Context context, AttributeSet attrs) { this(context, attrs, android.R.style.Theme_Black); } public SlideStackView(Context context) { this(context, null); }
/** * Same as {@link #getChildAt(int)} but uses adapter's * slide order in depending on the actual child order * in the view group * @param position position of the child to be retrieved * @return view in specified */ private View getChild(int position){ return getChildAt(getChildCount() - 1 - position); }
/** * Use this to manage child drawing order. We should draw * the slide #{lastSlide} (in adapter's data set indexes) first. Then draw * the slide with #{lastSlide - 1} and so on until the slide #0 inclusive * <p> * {@inheritDoc} */ @Override protected int getChildDrawingOrder(int childCount, int i) { //draw the last slide first. /** * __________ * __|0 | * |1 | | * | | | * | |__________| * |__________| */ return /*childCount - */i /*- 1*/; }
/** * Used in case we have no valid position or actual position * can not be found. */ static final int INVALID_POSITION = -1; /** * Index of the first element from adapter's data set * added to the layout of the slide stack */ private int mFirst; /** * Current selected slide position in adapter's data set */ private int mSelected; /** * A data set adapter that prepares view for the slide stack view * and is responsible for base information about the containing * data set. */ private SlideStateAdapter mAdapter; /** * {@link DataSetObserver} that indicates about changes in slides * data set */ private final DataSetObserver mObserver = new Observer(); /** * Sets the adapter for providing the SlideStackView with * slides. * @param adapter */ public void setAdapter(SlideStateAdapter adapter){ if (mAdapter != null){ mAdapter.unregDataSetObserver(mObserver); mFirst = INVALID_POSITION; mScroller.stopScrolling(); removeAllViews(); } if(adapter != null){ mAdapter = adapter; mAdapter.regDataSetObserver(mObserver); } }
private final class Observer extends DataSetObserver{ @Override public void onChanged() { //empty } @Override public void onInvalidated() { //empty } }
public static class SlideScroller extends Scroller implements OnTouchListener{ private final ScrollingListener mListener; private final GestureDetector mGestureDetector; public SlideScroller(Context context, ScrollingListener listener, OnGestureListener gestureListener) { super(context); this.mListener = listener; this.mGestureDetector = new GestureDetector(context, gestureListener); mGestureDetector.setIsLongpressEnabled(false); } public void scroll(int distance, int time) { // ... } public void fling(int velocity){ ... } public void stopScrolling() { ... } @Override public boolean onTouch(View v, MotionEvent event) { ... } void finishScrolling() { ... } boolean isScrolling(){ ... } boolean isJustifying(){ ... } boolean isTouchScrolling(){ ... } }
public interface ScrollingListener { void onScroll(int distance); void onStarted(); void onFinished(); void onJustify(); }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec)); int childHeightSize = getMeasuredHeight(); int mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec( childHeightSize, MeasureSpec.EXACTLY); // Make sure we have created all fragments that we need to have shown. mInLayout = true; fillViewsIn(); mInLayout = false; // measure slides int size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChild(i); if (child.getVisibility() != GONE) { int childWidthSize = getMeasuredWidth() - ( getRightEdgeDelta(mFirst + i - 1) + getLeftEdge(mFirst + i)); final int widthSpec = MeasureSpec.makeMeasureSpec( childWidthSize, MeasureSpec.EXACTLY); // LOG.v("Measuring #" + i + " " + child // + ": " + widthSpec); child.measure(widthSpec, mChildHeightMeasureSpec); } } }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mInLayout = true; fillViewsIn(); mInLayout = false; for (int i = 0; i < getChildCount(); i++) { View child = getChild(i); int position = i + mFirst; onLayoutChild(child, position, changed); } mDirty.setEmpty(); } /** * Layouts child at the specified position (in adapter's data set). * Measures the child if needed. * @param child a child we are going to layout * @param position position of the child in adapter's data set */ protected void onLayoutChild(View child, int position, boolean changed) { if (child.getVisibility() != GONE) { LOG.d("onLayoutChild " + position); if (position < mSelected && changed){ closeView(position); LOG.v("close slide at " + position); } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); SlideInfo info = getSlideInfo(child); int childLeft = getLeftEdge(position) + info.mOffset; int childRight = getRightEdge(position - 1) + info.mOffset; int childTop = getTop(); if (lp.needsMeasure) { lp.needsMeasure = false; final int widthSpec = MeasureSpec.makeMeasureSpec( childRight - childLeft, MeasureSpec.EXACTLY); final int heightSpec = MeasureSpec.makeMeasureSpec( getMeasuredHeight(), MeasureSpec.EXACTLY); child.measure(widthSpec, heightSpec); } // LOG.v("Positioning #" + position + " " + child + ":" + childLeft // + "," + childTop + " " + child.getMeasuredWidth() + "x" // + child.getMeasuredHeight()); child.layout(childLeft, getTop(), childRight, getBottom()); } }
/** * Simple info holder for the slide item * @author k.kharkov */ public static final class SlideInfo implements Parcelable{ /** * Describes slide offset relative to the slide stack. * Note that offset value do not describe actual slide * position inside the slide stack but only shows the * offset relative to the left position of the slide. * <p> * This means * <code>getLeftEdge(position) + info.mOffset</code> * equals to actual offset of the slide relative to * the slide stack view. */ private int mOffset = 0; /** * Indicates whether the slide is visible to the user * or hidden at near the slide stack side */ private boolean mOpened = true; /** * Position of the slide inside the slide stack */ private int mPosition = INVALID_POSITION; /** * The drawable to fill space between this slide and the * previous one. * @see SlideStackView#fillAreaToPrevSlide(Canvas, View) */ private Drawable mSpace; public SlideInfo() { super(); } }
/** * Adds all required views in layout first. * Then adjusts visibility for each child. * @see #addViewsInside(int, int) * @see #adjustViewsVisibility() */ private void fillViewsIn() { int position = 0; int left = 0; if (getChildCount() > 0){ position = getLastVisiblePosition() + 1; View lastView = getChild(position - mFirst - 1); left = lastView.getLeft() - lastView.getScrollX(); } addViewsInside(position, left); adjustViewsVisibility(); }
/** * Uses the adapter to add views to the slide stack view. * @param position last visible position of the view * @param left left coordinate of the last visible slide */ private void addViewsInside(int position, int left) { if (mAdapter == null || mAdapter.getCount() == 0){ return; } mAdapter.startUpdate(this); while ((position <= mSelected + 1 || left > getLeft()) && position < mAdapter.getCount()){ if (mFirst == INVALID_POSITION){ mFirst = 0; } LOG.d("addView inside " + position + " mSelected " + mSelected); mAdapter.instantiateItem(this, position); left = mSelected > position ? getLeftEdge(position) : getRightEdge(position - 1); position ++; } mAdapter.finishUpdate(this); }
/** * Specifies correct layout parameters for the child and * adds it according the the current {@link #mInLayout} * status. * <p> * {@inheritDoc} */ @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { LOG.d("Add view from outside " + child); if (!checkLayoutParams(params)) { params = generateLayoutParams(params); } final LayoutParams lp = (LayoutParams) params; SlideInfo info = getSlideInfo(child); info.mPosition = getChildCount(); if (mAdapter != null){ info.mSpace = mAdapter.getSpaceDrawable(info.mPosition); } if (mInLayout) { lp.needsMeasure = true; addViewInLayout(child, 0, params); } else { super.addView(child, 0, params); } if (info.mPosition < mSelected){ closeView(info.mPosition); } }
/** * Sets visibility parameter for each child according * to the actual visibility on the screen. Takes into * account that child shouldn't be invisible if it's * shadow is visible on the screen because it would * prevent from triggering {@link #drawChild(Canvas, View, long)} * method over that child. */ private void adjustViewsVisibility() { /** * init the rect to align right edge if the slide stack view */ Rect drawingRect = new Rect(); drawingRect.left = getRight(); drawingRect.top = getTop(); drawingRect.right = getRight(); drawingRect.bottom = getTop() + getHeight(); Rect parent = getChildDrawingRectPositive(this); /** * Then union each child drawing rect * with drawingRect of the slide stack * in order to determine when the views * behind the last actually visible view * in the slideStack view and hide all * the following views in order to prevent * re-drawing non-visible views * ________________________________ * | slideStackView __________| * | ____________________| | * || _ _ _ _ | | * ||| | |visible | * || |slide #0 | * ||| hidden slide| | | * || #2 | | * |||_ _ _ _| | | * ||last actual visible | | * ||slide #1 |__________| * ||__________________________| | * |________________________________| */ for (int i = 0; i < getChildCount(); i ++){ boolean hideNext = false; // LOG.v("adjustVisibility " + i); View child = getChild(i); Rect childRect = getChildDrawingRectPositive(child); // LOG.v("child.left " + childRect.left + // " drawing.left" + drawingRect.left + // " parent " + parent.toShortString() + // " child" + childRect.toShortString()); if (childRect.left < drawingRect.left && Rect.intersects(parent, childRect)){ drawingRect.union(childRect); // LOG.d("drawingRect after union with child " + i + " = " + // drawingRect.toShortString()); hideNext = false; } else { // LOG.d("hide view " + i); // LOG.v("child " + childRect.toShortString() + // " drawing " + drawingRect.toShortString() + // " parent " + parent.toShortString()); Rect childShadow = getShadowRect(childRect); if (childShadow.isEmpty()){ hideNext = true; } // if shadow visible - do not hide the slide // and let the shadow to be drawn if (childShadow.left < drawingRect.left && Rect.intersects(parent, childShadow)){ hideNext = false; } else { hideNext = true; } } // LOG.d("set visibility for child " + i + " = " + // (hideNext ? "Invisible" : "Visible")); child.setVisibility(hideNext ? View.INVISIBLE : View.VISIBLE); } }
Rect childShadow = getShadowRect(childRect); if (childShadow.isEmpty()){ hideNext = true; } // if shadow visible - do not hide the slide // and let the shadow to be drawn if (childShadow.left < drawingRect.left && Rect.intersects(parent, childShadow)){ hideNext = false; } else { hideNext = true; }
/** * Clips the canvas to the child current bounds plus shadow. * <p> * Draws the shadow as well for each child. * <p> * {@inheritDoc} * @see #applyShadowToChild(Canvas, View) */ @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { Rect childClip = getChildClipRect(child); // LOG.d("canvas " + canvas.getClipBounds().toShortString()); // LOG.d("draw Child " + child + " clip = " + childClip.toShortString()); // first // draw spaces between slides fillAreaToPrevSlide(canvas, child); // second // draw shadow for the slide applyShadowToChild(canvas, child); // third // actually draw child try{ canvas.save(); canvas.clipRect(childClip); boolean r = super.drawChild(canvas, child, drawingTime); return r; } finally { canvas.restore(); } }
/** * Retrieves children's visible position on the screen * without it's shadow. * @param child we should retrieve visible bounds for * @return a child's visible bounds */ private Rect getChildClipRect(View child) { Rect childClip = getChildDrawingRectPositive(child); int position = getPositionForView(child); subtractRectWithChilds(childClip, position); // LOG.v("child clip " + position + " " + childClip.toShortString()); return childClip; } /** * Changes the specified clip rectangle, to subtract all the * following children from it. * @param childClip initial child rectangle in the screen * @param position child position within adapter's data set * @see #subtractToLeft(Rect, Rect) */ private void subtractRectWithChilds(Rect childClip, int position) { if (position >= 0){ position -= mFirst; for (int i = position - 1; i >= 0; i --){ View c = getChild(i); Rect r = getChildDrawingRectPositive(c); boolean changed = subtractToLeft(childClip, r); if (changed){ // LOG.v("child clipped " + childClip.toShortString()); } } } }
/** * Same as {@link #subtract(Rect, Rect)} method, but processes * the case where source rectangle wasn't changed because it * contains <code>r</code>. In this case it adjusts <code>r</code> * from this: * <pre> * _______________________ * | source _________ | * | | r | | * | | | | * | | | | * | | | | * | |_________| | * |_______________________| * </pre> * * to this: in order to leave only left side of the source rectangle. * <pre> * ___________________________ * | source | r |1px| * | | |<->| * | | | | * | | | | * | | | | * | | | | * | | | | * |_________|_____________|___| * </pre> * @param source the rectangle we are going to subtract from * @param r the rectangle we are going to subtract from * source * @return <code>true</code> in case the source rectangle * has been changed. <code>false</code> otherwise */ private static boolean subtractToLeft(Rect source, Rect r){ boolean changed = subtract(source, r); if (!changed && source.contains(r)){ // adjust intersected rect manually r.right = source.right + 1; r.top = source.top; r.bottom = source.bottom; changed = subtract(source, r); } return changed; } /** * Subtracts <code>r</code> from the <code>source</code>. * Sets <code>r</code> rectangle to the intersection as well. * @param source * @param r * @return <code>true</code> if rectangle has been subtracted, * <code>false</code> otherwise. */ private static boolean subtract(Rect source, Rect r) { if (source.isEmpty() || r.isEmpty()){ return false; } if (r.contains(source)){ // LOG.w("return empty rect"); source.setEmpty(); return true; } if (source.contains(r)){ return false; } if (!r.intersect(source)){ return false; } boolean changed = false; if (source.left == r.left){ source.left = r.right; changed = true; } if (source.right == r.right){ source.right = r.left; changed = true; } if (source.top == r.top){ source.top = r.bottom; changed = true; } if (source.bottom == r.bottom){ source.bottom = r.top; changed = true; } source.sort(); return changed; }
Source: https://habr.com/ru/post/182750/
All Articles