Java tutorial
/* * Copyright (C) 2017 WordPlat Open Source Project * * https://wordplat.com/InteractiveKLineView/ * * 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.wordplat.ikvstockchart; import android.content.Context; import android.graphics.Canvas; import android.graphics.RectF; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v4.widget.ScrollerCompat; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.view.ViewConfiguration; import android.view.animation.Interpolator; import com.wordplat.ikvstockchart.compat.ViewUtils; import com.wordplat.ikvstockchart.entry.Entry; import com.wordplat.ikvstockchart.entry.EntrySet; import com.wordplat.ikvstockchart.entry.StockDataTest; import com.wordplat.ikvstockchart.compat.GestureMoveActionCompat; import com.wordplat.ikvstockchart.render.AbstractRender; import com.wordplat.ikvstockchart.render.KLineRender; /** * <p>? K </p> * <p>Date: 2017/3/10</p> * * @author afon */ public class InteractiveKLineView extends View { private static final String TAG = "InteractiveKLineView"; private static final boolean DEBUG = false; // ? private final RectF viewRect; private final float viewPadding; // ?? private AbstractRender render; private EntrySet entrySet; private KLineHandler kLineHandler; // ?? private static final int OVERSCROLL_DURATION = 500; // dragging ???? private static final int OVERSCROLL_THRESHOLD = 220; // dragging ???? private static final int KLINE_STATUS_IDLE = 0; // private static final int KLINE_STATUS_RELEASE_BACK = 2; // loading ? private static final int KLINE_STATUS_LOADING = 3; // private static final int KLINE_STATUS_SPRING_BACK = 4; // ??? private int kLineStatus = KLINE_STATUS_IDLE; private int lastFlingX = 0; private int lastScrollDx = 0; private int lastEntrySize = 0; // entry ???? private int lastHighlightIndex = -1; // entry ? private final ScrollerCompat scroller; // private boolean onTouch = false; private boolean onLongPress = false; private boolean onDoubleFingerPress = false; private boolean onVerticalMove = false; private boolean onDragging = false; private boolean enableLeftRefresh = true; private boolean enableRightRefresh = true; public InteractiveKLineView(Context context) { this(context, null); } public InteractiveKLineView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public InteractiveKLineView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); viewRect = new RectF(); viewPadding = ViewUtils.dpTopx(context, 10); render = new KLineRender(context); gestureDetector.setIsLongpressEnabled(true); int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); gestureCompat.setTouchSlop(touchSlop); final Interpolator interpolator = new Interpolator() { public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; scroller = ScrollerCompat.create(context, interpolator); render.setSizeColor(ViewUtils.getSizeColor(context, attrs, defStyleAttr)); } public void setEntrySet(EntrySet set) { entrySet = set; } public void notifyDataSetChanged() { notifyDataSetChanged(true); } public void notifyDataSetChanged(boolean invalidate) { render.setViewRect(viewRect); render.onViewRect(viewRect); render.setEntrySet(entrySet); if (invalidate) { invalidate(); } } public void setRender(AbstractRender render) { render.setSizeColor(this.render.getSizeColor()); this.render = render; } public AbstractRender getRender() { return render; } public void setKLineHandler(KLineHandler kLineHandler) { this.kLineHandler = kLineHandler; } public RectF getViewRect() { return viewRect; } private final GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent e) { if (onTouch) { onLongPress = true; highlight(e.getX(), e.getY()); } } @Override public boolean onDoubleTap(MotionEvent e) { if (kLineHandler != null) { kLineHandler.onDoubleTap(e, e.getX(), e.getY()); } return true; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (kLineHandler != null) { kLineHandler.onSingleTap(e, e.getX(), e.getY()); } return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (!onLongPress && !onDoubleFingerPress && !onVerticalMove) { if (onDragging && !render.canScroll(distanceX) && render.canDragging(distanceX)) { dragging((int) distanceX); if (DEBUG) { Log.v(TAG, "##d dragging: -------> " + distanceX + ", maxScrollOffset = " + render.getMaxScrollOffset() + ", tranX = " + render.getCurrentTransX()); } } else { scroll((int) distanceX); if (DEBUG) { Log.v(TAG, "##d scroll: -------> " + distanceX + ", maxScrollOffset = " + render.getMaxScrollOffset() + ", tranX = " + render.getCurrentTransX()); } } return true; } else { return false; } } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { lastFlingX = 0; if (!onLongPress && !onDoubleFingerPress && !onVerticalMove && render.canScroll(0)) { if (DEBUG) { Log.d(TAG, "##d onFling: ------->"); } scroller.fling(0, 0, (int) -velocityX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); return true; } else { return false; } } }); private final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() { @Override public void onScaleEnd(ScaleGestureDetector detector) { float f = detector.getScaleFactor(); if (f < 1.0f) { render.zoomOut(detector.getFocusX(), detector.getFocusY()); } else if (f > 1.0f) { render.zoomIn(detector.getFocusX(), detector.getFocusY()); } } }); private GestureMoveActionCompat gestureCompat = new GestureMoveActionCompat(null); private void highlight(float x, float y) { render.onHighlight(x, y); invalidate(); int highlightIndex = render.getEntrySet().getHighlightIndex(); Entry entry = render.getEntrySet().getHighlightEntry(); if (entry != null && lastHighlightIndex != highlightIndex) { if (kLineHandler != null) { kLineHandler.onHighlight(entry, highlightIndex, x, y); } lastHighlightIndex = highlightIndex; } } private void cancelHighlight() { render.onCancelHighlight(); invalidate(); if (kLineHandler != null) { kLineHandler.onCancelHighlight(); } lastHighlightIndex = -1; } /** * ?? * * @param dx ?? */ public void scroll(float dx) { render.scroll(dx); invalidate(); } /** * ?? K ???? * * @param dx ?? */ private void dragging(float dx) { if (render.getMaxScrollOffset() < 0 || dx < 0) { render.updateCurrentTransX(dx); render.updateOverScrollOffset(dx); invalidate(); } } /** * ??? * * @param dx ?? */ private void releaseBack(float dx) { render.updateCurrentTransX(dx); render.updateOverScrollOffset(dx); invalidate(); } /** * ???? * * @param dx ?? */ private void springBack(float dx) { if (entrySet.getEntryList().size() > lastEntrySize) { scroll(dx); } else { releaseBack(dx); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { viewRect.set(viewPadding, viewPadding, w - viewPadding, h - viewPadding); if (DEBUG) { Log.i(TAG, "##d onSizeChanged: " + viewRect); } // Android Studio ??? K ? if (isInEditMode()) { EntrySet entrySet = StockDataTest.parseKLineData(StockDataTest.KLINE); if (entrySet != null) { entrySet.computeStockIndex(); } setEntrySet(entrySet); } if (entrySet == null) { entrySet = new EntrySet(); } notifyDataSetChanged(); } @Override public void computeScroll() { if (onVerticalMove) { return; } if (scroller.computeScrollOffset()) { final int x = scroller.getCurrX(); final int dx = x - lastFlingX; lastFlingX = x; if (onTouch) { scroller.abortAnimation(); } else { if (kLineStatus == KLINE_STATUS_RELEASE_BACK) { releaseBack(dx); } else if (kLineStatus == KLINE_STATUS_SPRING_BACK) { springBack(dx); } else { scroll(dx); } if (render.canScroll(dx) && !scroller.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } } if (DEBUG) { Log.i(TAG, "##d computeScrollOffset: canScroll = " + render.canScroll(dx) + ", overScrollOffset = " + render.getOverScrollOffset() + ", dx = " + dx + ", tranX = " + render.getCurrentTransX()); } } else { final float overScrollOffset = render.getOverScrollOffset(); if (DEBUG) { Log.d(TAG, "##d overScrollOffset: canScroll = " + render.canScroll(0) + ", overScrollOffset = " + overScrollOffset); } if (!onTouch && overScrollOffset != 0 && kLineStatus == KLINE_STATUS_IDLE) { lastScrollDx = 0; float dx = overScrollOffset; if (Math.abs(overScrollOffset) > OVERSCROLL_THRESHOLD) { if (enableLeftRefresh && overScrollOffset > 0) { lastScrollDx = (int) overScrollOffset - OVERSCROLL_THRESHOLD; dx = lastScrollDx; } if (enableRightRefresh && overScrollOffset < 0) { lastScrollDx = (int) overScrollOffset + OVERSCROLL_THRESHOLD; dx = lastScrollDx; } } if (DEBUG) { Log.d(TAG, "##d startScroll: LOADING... dx = " + dx); } kLineStatus = KLINE_STATUS_RELEASE_BACK; lastFlingX = 0; scroller.startScroll(0, 0, (int) dx, 0, OVERSCROLL_DURATION); ViewCompat.postInvalidateOnAnimation(this); } else if (kLineStatus == KLINE_STATUS_RELEASE_BACK) { kLineStatus = KLINE_STATUS_LOADING; if (kLineHandler != null) { lastEntrySize = entrySet.getEntryList().size(); if (lastScrollDx > 0) { kLineHandler.onLeftRefresh(); } else if (lastScrollDx < 0) { kLineHandler.onRightRefresh(); } } else { refreshComplete(); } } else { kLineStatus = KLINE_STATUS_IDLE; } } } /** * ? */ public void refreshComplete() { refreshComplete(false); } /** * ? * * @param reverse ???? */ public void refreshComplete(boolean reverse) { final int overScrollOffset = (int) render.getOverScrollOffset(); if (DEBUG) { Log.i(TAG, "##d refreshComplete: refreshComplete... overScrollOffset = " + overScrollOffset); } if (overScrollOffset != 0) { kLineStatus = KLINE_STATUS_SPRING_BACK; lastFlingX = 0; scroller.startScroll(0, 0, reverse ? -overScrollOffset : overScrollOffset, 0, OVERSCROLL_DURATION); ViewCompat.postInvalidateOnAnimation(this); } } public boolean isRefreshing() { return kLineStatus == KLINE_STATUS_LOADING; } public void setEnableLeftRefresh(boolean enableLeftRefresh) { this.enableLeftRefresh = enableLeftRefresh; } public void setEnableRightRefresh(boolean enableRightRefresh) { this.enableRightRefresh = enableRightRefresh; } public boolean isHighlighting() { return render.isHighlight(); } @Override public boolean dispatchTouchEvent(MotionEvent event) { boolean onHorizontalMove = gestureCompat.onTouchEvent(event, event.getX(), event.getY()); final int action = MotionEventCompat.getActionMasked(event); onVerticalMove = false; if (action == MotionEvent.ACTION_MOVE) { if (!onHorizontalMove && !onLongPress && !onDoubleFingerPress && gestureCompat.isDragging()) { onTouch = false; onVerticalMove = true; } } getParent().requestDisallowInterceptTouchEvent(!onVerticalMove); return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent e) { final int action = MotionEventCompat.getActionMasked(e); gestureDetector.onTouchEvent(e); scaleDetector.onTouchEvent(e); switch (action) { case MotionEvent.ACTION_DOWN: { onTouch = true; onDragging = false; break; } case MotionEvent.ACTION_POINTER_DOWN: { onDoubleFingerPress = true; break; } case MotionEvent.ACTION_MOVE: { onDragging = true; if (onLongPress) { highlight(e.getX(), e.getY()); } break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { onLongPress = false; onDoubleFingerPress = false; onTouch = false; onDragging = false; cancelHighlight(); break; } } return true; } @Override protected void onDraw(Canvas canvas) { render.render(canvas); } }