Java tutorial
/* * Copyright 2013, Edmodo, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. * You may obtain a copy of the License in the LICENSE file, or 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.dgmltn.ranger.internal; /* * Copyright 2015, Appyvet, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. * You may obtain a copy of the License in the LICENSE file, or 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. */ import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; import android.os.Bundle; import android.os.Parcelable; import android.support.v4.util.Pools; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import com.dgmltn.ranger.R; import java.util.ArrayList; /** * Traits taken from the following files: * https://github.com/android/platform_frameworks_base/blob/master/core/java/android/preference/SeekBarPreference.java * contains https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/SeekBar.java * extends https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/AbsSeekBar.java * extends https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ProgressBar.java * extends View https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/View.java * https://github.com/dgmltn/material-range-bar/blob/master/lib/src/com/dgmltn/ranger/internal/AbsRangeBar.java * a fork of https://github.com/oli107/material-range-bar/blob/master/rangebar/src/com/appyvet/rangebar/RangeBar.java * a fork of https://github.com/edmodo/range-bar/blob/master/rangebar/src/com/edmodo/rangebar/RangeBar.java * <p/> * The MaterialRangeBar is a single or double-sided version of a {@link android.widget.SeekBar} with * discrete values. Whereas the thumb for the SeekBar can be dragged to any position in the bar, the * RangeBar only allows its thumbs to be dragged to discrete positions (denoted by tick marks) in * the bar. When released, a RangeBar thumb will snap to the nearest tick mark. This version is * forked from edmodo range bar: * https://github.com/edmodo/range-bar.git * Clients of the RangeBar can attach a {@link AbsRangeBar.OnRangeBarChangeListener} to be notified * when the pins have been moved. */ public abstract class AbsRangeBar extends View { // Member Variables //////////////////////////////////////////////////////// //private static final String TAG = PbLog.TAG(AbsRangeBar.class); // Default values for variables private static final int DEFAULT_TICK_COUNT = 5; private static final float DEFAULT_TICK_SIZE_DP = 1; private static final float DEFAULT_PIN_PADDING_DP = 16; public static final float DEFAULT_MIN_PIN_FONT_SP = 8; public static final float DEFAULT_MAX_PIN_FONT_SP = 24; private static final float DEFAULT_BAR_WEIGHT_DP = 1; private static final int DEFAULT_BAR_COLOR = Color.LTGRAY; private static final int DEFAULT_TEXT_COLOR = Color.WHITE; private static final int DEFAULT_TICK_COLOR = Color.BLACK; private static final int INDIGO_500 = 0xff3f51b5; private static final int DEFAULT_PIN_COLOR = INDIGO_500; private static final float DEFAULT_CONNECTING_LINE_WEIGHT_DP = 2; private static final int DEFAULT_CONNECTING_LINE_COLOR = INDIGO_500; private static final float DEFAULT_EXPANDED_PIN_RADIUS_DP = 12; private static final float DEFAULT_CIRCLE_SIZE_DP = 5; // "natural" dimensions of this View for WRAP_CONTENT private static final int DEFAULT_WIDTH = 500; private static final int DEFAULT_HEIGHT = 150; // Instance variables for all of the customizable attributes // Bar private float mBarWeight; private int mBarColor; // Ticks private float mTickSize; private int mTickColor; private int mTickCount; private boolean mDrawTicks = true; // Selectors private int mFirstSelectorColor; private int mSecondSelectorColor; private float mSelectorSize; // Pins private final PinView mFirstPinView; private final PinView mSecondPinView; private int mFirstPinColor; private int mSecondPinColor; private float mPinRadius; private float mExpandedPinRadius; private float mMinPinFont; private float mMaxPinFont; private float mPinPadding; private int mFirstPinTextColor; private int mSecondPinTextColor; private boolean mArePinsTemporary = true; // Connecting Line private float mConnectingLineWeight; protected int mFirstConnectingLineColor; protected int mSecondConnectingLineColor; // Listeners private OnRangeBarChangeListener mOnRangeBarChangeListener; private boolean mIsRangeBar = true; private int mActiveFirstConnectingLineColor; private int mActiveSecondConnectingLineColor; private int mActiveBarColor; private int mActiveTickColor; private int mActiveFirstCircleColor; private int mActiveSecondCircleColor; protected boolean mConnectingLineInverted; private IndexFormatter mIndexFormatter = new IndexFormatter() { @Override public String getLabel(int index) { String value = Integer.toString(index); if (value.length() > 4) { return value.substring(0, 4); } else { return value; } } }; // TODO:(pv) Consider removing most if not all of these... private final long mUiThreadId; private RefreshProgressRunnable mRefreshProgressRunnable; private boolean mAttached; private boolean mRefreshIsPosted; private final ArrayList<RefreshData> mRefreshData = new ArrayList<RefreshData>(); // Constructors //////////////////////////////////////////////////////////// public AbsRangeBar(Context context) { this(context, null); } public AbsRangeBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public AbsRangeBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mUiThreadId = Thread.currentThread().getId(); mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mFirstPinView = new PinView(context); mFirstPinView.setName("mFirstPinView"); mSecondPinView = new PinView(context); mSecondPinView.setName("mSecondPinView"); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.AbsRangeBar); initialize(ta); } public void initialize(TypedArray ta) { initialize(ta, true); } public void initialize(TypedArray ta, boolean recycle) { Context context = getContext(); try { // Sets the values of the user-defined attributes based on the XML attributes. int tickCount = ta.getInt(R.styleable.AbsRangeBar_tickCount, DEFAULT_TICK_COUNT); validateTickCount(tickCount); mTickCount = tickCount; float density = context.getResources().getDisplayMetrics().density; mTickSize = ta.getDimension(R.styleable.AbsRangeBar_tickHeight, DEFAULT_TICK_SIZE_DP * density); mBarWeight = ta.getDimension(R.styleable.AbsRangeBar_barWeight, DEFAULT_BAR_WEIGHT_DP * density); mBarColor = ta.getColor(R.styleable.AbsRangeBar_rangeBarColor, DEFAULT_BAR_COLOR); mActiveBarColor = mBarColor; int pinTextColor = ta.getColor(R.styleable.AbsRangeBar_textColor, DEFAULT_TEXT_COLOR); mFirstPinTextColor = pinTextColor; mSecondPinTextColor = pinTextColor; int pinColor = ta.getColor(R.styleable.AbsRangeBar_pinColor, DEFAULT_PIN_COLOR); mFirstPinColor = pinColor; mSecondPinColor = pinColor; mSelectorSize = ta.getDimension(R.styleable.AbsRangeBar_selectorSize, DEFAULT_CIRCLE_SIZE_DP * density); int selectorColor = ta.getColor(R.styleable.AbsRangeBar_selectorColor, DEFAULT_CONNECTING_LINE_COLOR); mFirstSelectorColor = selectorColor; mSecondSelectorColor = selectorColor; mActiveFirstCircleColor = selectorColor; mActiveSecondCircleColor = selectorColor; mTickColor = ta.getColor(R.styleable.AbsRangeBar_tickColor, DEFAULT_TICK_COLOR); mActiveTickColor = mTickColor; mConnectingLineWeight = ta.getDimension(R.styleable.AbsRangeBar_connectingLineWeight, DEFAULT_CONNECTING_LINE_WEIGHT_DP * density); int connectingLineColor = ta.getColor(R.styleable.AbsRangeBar_connectingLineColor, DEFAULT_CONNECTING_LINE_COLOR); mFirstConnectingLineColor = connectingLineColor; mSecondConnectingLineColor = connectingLineColor; mActiveFirstConnectingLineColor = connectingLineColor; mActiveSecondConnectingLineColor = connectingLineColor; mExpandedPinRadius = ta.getDimension(R.styleable.AbsRangeBar_pinRadius, DEFAULT_EXPANDED_PIN_RADIUS_DP * density); mPinPadding = ta.getDimension(R.styleable.AbsRangeBar_pinPadding, DEFAULT_PIN_PADDING_DP * density); mIsRangeBar = ta.getBoolean(R.styleable.AbsRangeBar_rangeBar, true); mArePinsTemporary = ta.getBoolean(R.styleable.AbsRangeBar_temporaryPins, true); float scaledDensity = getResources().getDisplayMetrics().scaledDensity; mMinPinFont = ta.getDimension(R.styleable.AbsRangeBar_pinMinFont, DEFAULT_MIN_PIN_FONT_SP * scaledDensity); mMaxPinFont = ta.getDimension(R.styleable.AbsRangeBar_pinMaxFont, DEFAULT_MAX_PIN_FONT_SP * scaledDensity); } finally { if (recycle) { ta.recycle(); } } initBar(); initPins(true); setTickCount(mTickCount); } // View Methods //////////////////////////////////////////////////////////// @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable("instanceState", super.onSaveInstanceState()); bundle.putFloat("BAR_WEIGHT", mBarWeight); bundle.putInt("BAR_COLOR", mBarColor); bundle.putInt("TICK_COUNT", mTickCount); bundle.putInt("TICK_COLOR", mTickColor); bundle.putFloat("TICK_SIZE", mTickSize); bundle.putFloat("CONNECTING_LINE_WEIGHT", mConnectingLineWeight); bundle.putInt("FIRST_CONNECTING_LINE_COLOR", mFirstConnectingLineColor); bundle.putInt("SECOND_CONNECTING_LINE_COLOR", mSecondConnectingLineColor); bundle.putFloat("SELECTOR_SIZE", mSelectorSize); bundle.putInt("FIRST_SELECTOR_COLOR", mFirstSelectorColor); bundle.putInt("SECOND_SELECTOR_COLOR", mSecondSelectorColor); bundle.putFloat("PIN_RADIUS", mPinRadius); bundle.putFloat("EXPANDED_PIN_RADIUS", mExpandedPinRadius); bundle.putFloat("PIN_PADDING", mPinPadding); bundle.putBoolean("IS_RANGE_BAR", mIsRangeBar); bundle.putBoolean("ARE_PINS_TEMPORARY", mArePinsTemporary); bundle.putInt("FIRST_PIN_INDEX", getFirstPinIndex()); bundle.putInt("SECOND_PIN_INDEX", getSecondPinIndex()); bundle.putFloat("MIN_PIN_FONT", mMinPinFont); bundle.putFloat("MAX_PIN_FONT", mMaxPinFont); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; mBarWeight = bundle.getFloat("BAR_WEIGHT"); mBarColor = bundle.getInt("BAR_COLOR"); mTickCount = bundle.getInt("TICK_COUNT"); mTickColor = bundle.getInt("TICK_COLOR"); mTickSize = bundle.getFloat("TICK_SIZE"); mConnectingLineWeight = bundle.getFloat("CONNECTING_LINE_WEIGHT"); mFirstConnectingLineColor = bundle.getInt("FIRST_CONNECTING_LINE_COLOR"); mSecondConnectingLineColor = bundle.getInt("SECOND_CONNECTING_LINE_COLOR"); mSelectorSize = bundle.getFloat("SELECTOR_SIZE"); mFirstSelectorColor = bundle.getInt("FIRST_SELECTOR_COLOR"); mSecondSelectorColor = bundle.getInt("SECOND_SELECTOR_COLOR"); mPinRadius = bundle.getFloat("PIN_RADIUS"); mExpandedPinRadius = bundle.getFloat("EXPANDED_PIN_RADIUS"); mPinPadding = bundle.getFloat("PIN_PADDING"); mIsRangeBar = bundle.getBoolean("IS_RANGE_BAR"); mArePinsTemporary = bundle.getBoolean("ARE_PINS_TEMPORARY"); int firstPinIndex = bundle.getInt("FIRST_PIN_INDEX"); int secondPinIndex = bundle.getInt("SECOND_PIN_INDEX"); setPinIndices(firstPinIndex, secondPinIndex); mMinPinFont = bundle.getFloat("MIN_PIN_FONT"); mMaxPinFont = bundle.getFloat("MAX_PIN_FONT"); super.onRestoreInstanceState(bundle.getParcelable("instanceState")); } else { super.onRestoreInstanceState(state); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width; int height; // Get measureSpec mode and size values. final int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec); final int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec); final int measureWidth = MeasureSpec.getSize(widthMeasureSpec); final int measureHeight = MeasureSpec.getSize(heightMeasureSpec); // The RangeBar width should be as large as possible. if (measureWidthMode == MeasureSpec.AT_MOST) { width = measureWidth; } else if (measureWidthMode == MeasureSpec.EXACTLY) { width = measureWidth; } else { width = DEFAULT_WIDTH; } // The RangeBar height should be as small as possible. if (measureHeightMode == MeasureSpec.AT_MOST) { height = Math.min(DEFAULT_HEIGHT, measureHeight); } else if (measureHeightMode == MeasureSpec.EXACTLY) { height = measureHeight; } else { height = DEFAULT_HEIGHT; } setMeasuredDimension(width, height); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // This is the initial point at which we know the size of the View. resizeBar(w, h); initPins(false); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawBar(canvas); if (mDrawTicks) { drawTicks(canvas); } drawConnectingLine(canvas, mFirstPinView, mSecondPinView); mFirstPinView.draw(canvas); if (mIsRangeBar) { mSecondPinView.draw(canvas); } } // Touch Methods //////////////////////////////////////////////////////////// private PinView mDraggingPin; private int mScaledTouchSlop; private PointF mTouchDown; private boolean mIsTrackingTouch; /** * Some logic ideas came from: * https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/AbsSeekBar.java#L564 * * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled()) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //PbLog.e(TAG, "onTouchEvent: ACTION_DOWN"); if (isInScrollingContainer()) { //PbLog.e(TAG, "onTouchEvent: ACTION_DOWN isInScrollingContainer() == true; waiting for slop"); mTouchDown = new PointF(event.getX(), event.getY()); } else { //PbLog.e(TAG, "onTouchEvent: ACTION_DOWN isInScrollingContainer() == false; start tracking/processing"); setPressed(true); //if (mThumb != null) { // invalidate(mThumb.getBounds()); // This may be within the padding region //} invalidate(); onStartTrackingTouch(); trackTouchEvent(event); attemptClaimDrag(); } break; case MotionEvent.ACTION_MOVE: //PbLog.e(TAG, "onTouchEvent: ACTION_MOVE"); if (mIsTrackingTouch) { //PbLog.e(TAG, "onTouchEvent: ACTION_MOVE mIsTrackingTouch == true; track/process"); trackTouchEvent(event); } else { final float x = event.getX(); if (mTouchDown != null && Math.abs(x - mTouchDown.x) > mScaledTouchSlop) { //PbLog.e(TAG, "onTouchEvent: ACTION_MOVE mIsTrackingTouch == false; slop exceeded; start tracking/processing"); setPressed(true); //if (mThumb != null) { // invalidate(mThumb.getBounds()); // This may be within the padding region //} invalidate(); onStartTrackingTouch(); trackTouchEvent(event); attemptClaimDrag(); } } break; case MotionEvent.ACTION_UP: //PbLog.e(TAG, "onTouchEvent: ACTION_UP"); if (mIsTrackingTouch) { //PbLog.e(TAG, "onTouchEvent: ACTION_UP mIsTrackingTouch == true; track/process and stop tracking/processing"); trackTouchEvent(event); onStopTrackingTouch(); setPressed(false); } else { //PbLog.e(TAG, "onTouchEvent: ACTION_UP mIsTrackingTouch == false; start and stop tracking/processing tap-seek"); // Touch up when we never crossed the touch slop threshold should // be interpreted as a tap-seek to that location. onStartTrackingTouch(); trackTouchEvent(event); onStopTrackingTouch(); } mTouchDown = null; // ProgressBar doesn't know to repaint the thumb drawable // in its inactive state when the touch stops (because the // value has not apparently changed) invalidate(); break; case MotionEvent.ACTION_CANCEL: //PbLog.e(TAG, "onTouchEvent: ACTION_CANCEL"); if (mIsTrackingTouch) { //PbLog.e(TAG, "onTouchEvent: ACTION_CANCEL mIsTrackingTouch == true; stop tracking/processing"); onStopTrackingTouch(); setPressed(false); } mTouchDown = null; invalidate(); // see above explanation break; } return true; } /** * From: * https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/View.java#L10407 * * @return */ public boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; } /** * From: * ... * Tries to claim the user's drag motion, and requests disallowing any * ancestors from stealing events in the drag. */ private void attemptClaimDrag() { ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } /** * From: * ... * This is called when the user has started touching this widget. */ protected void onStartTrackingTouch() { //PbLog.e(TAG, "onStartTrackingTouch()"); mIsTrackingTouch = true; if (mOnRangeBarChangeListener != null) { mOnRangeBarChangeListener.onStartTrackingTouch(this); } } /** * From: * ... * This is called when the user either releases his touch or the touch is * canceled. */ protected void onStopTrackingTouch() { //PbLog.e(TAG, "onStopTrackingTouch()"); mIsTrackingTouch = false; if (mOnRangeBarChangeListener != null) { mOnRangeBarChangeListener.onStopTrackingTouch(this); } } /** * From: * ... * * @param event */ private void trackTouchEvent(MotionEvent event) { //PbLog.e(TAG, "trackTouchEvent(event=" + event + ')'); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mDraggingPin = getTargetPinView(event.getX(), event.getY()); //PbLog.e(TAG, "trackTouchEvent: ACTION_DOWN mDraggingPin=" + mDraggingPin); if (mDraggingPin != null) { pressPin(mDraggingPin); } break; case MotionEvent.ACTION_MOVE: if (mDraggingPin == null && mTouchDown != null) { mDraggingPin = getTargetPinView(mTouchDown.x, mTouchDown.y); //PbLog.e(TAG, "trackTouchEvent: ACTION_MOVE mDraggingPin=" + mDraggingPin); if (mDraggingPin != null) { pressPin(mDraggingPin); } } if (mDraggingPin != null) { int nearestTickIndex = getNearestIndex(mDraggingPin); //PbLog.e(TAG, "trackTouchEvent: ACTION_MOVE nearestTickIndex=" + nearestTickIndex); PointF point = new PointF(event.getX(), event.getY()); int firstPinIndex = -1; int secondPinIndex = -1; if (mFirstPinView.equals(mDraggingPin)) { firstPinIndex = nearestTickIndex; secondPinIndex = mSecondPinView.getIndex(); } else if (mSecondPinView.equals(mDraggingPin)) { firstPinIndex = mFirstPinView.getIndex(); secondPinIndex = nearestTickIndex; } if (firstPinIndex != -1 && secondPinIndex != -1) { setPinIndices(firstPinIndex, secondPinIndex, mDraggingPin, point); } } break; case MotionEvent.ACTION_UP: if (mDraggingPin != null) { releasePin(mDraggingPin); mDraggingPin = null; } // else { // // Touch up when we never crossed the touch slop threshold should // // be interpreted as a tap-seek to that location. But let's not do that now. //} break; case MotionEvent.ACTION_CANCEL: if (mDraggingPin != null) { mDraggingPin = null; } break; } } // Public Methods ////////////////////////////////////////////////////////// /** * Sets a listener to receive notifications of changes to the RangeBar. This * will overwrite any existing set listeners. * * @param listener the RangeBar notification listener; null to remove any * existing listener */ public void setOnRangeBarChangeListener(OnRangeBarChangeListener listener) { mOnRangeBarChangeListener = listener; } /** * Sets an object to format pin values. * * @param formatter */ public void setIndexFormatter(IndexFormatter formatter) { mIndexFormatter = formatter; invalidate(); } /** * Enables or disables the drawing of tick marks. * * @param enable */ public void enableDrawTicks(boolean enable) { mDrawTicks = enable; invalidate(); } /** * Gets the tick count. * * @return the tick count */ public int getTickCount() { return mTickCount; } private void validateTickCount(int tickCount) { if (tickCount < 2) { throw new IllegalArgumentException("tickCount(" + tickCount + ") must be > 1"); } } /** * Sets up the ticks in the RangeBar. * * @param tickCount int specifying the number of ticks to display; must be > 1 */ public void setTickCount(int tickCount) { //PbLog.e(TAG, "setTickCount(" + tickCount + ')'); validateTickCount(tickCount); mTickCount = tickCount; boolean changed = false; int maxSecondIndex = mTickCount - 1; //PbLog.e(TAG, "setTickCount: maxSecondIndex=" + maxSecondIndex); int secondIndex = mSecondPinView.getIndex(); //PbLog.e(TAG, "setTickCount: secondIndex=" + secondIndex); if (secondIndex > maxSecondIndex) { changed = true; secondIndex = maxSecondIndex; } int maxFirstIndex = secondIndex - 1; //PbLog.e(TAG, "setTickCount: maxFirstIndex=" + maxFirstIndex); int firstIndex = mFirstPinView.getIndex(); //PbLog.e(TAG, "setTickCount: firstIndex=" + firstIndex); if (firstIndex > maxFirstIndex) { changed = true; firstIndex = maxFirstIndex; } if (changed) { setPinIndices(firstIndex, secondIndex, null, null); } invalidate(); } /** * Sets the size (radius) of the ticks in the range bar. * * @param size float size the height of each tick mark in px. */ public void setTickHeight(float size) { mTickSize = size; invalidate(); } /** * Set the weight of the bar line and the tick lines in the range bar. * * @param barWeight float specifying the weight of the bar and tick lines in * px. */ public void setBarWeight(float barWeight) { mBarWeight = barWeight; mBarPaint.setStrokeWidth(mBarWeight); invalidate(); } /** * Set the color of the bar line and the tick lines in the range bar. * * @param barColor Integer specifying the color of the bar line. */ public void setBarColor(int barColor) { mBarColor = barColor; mBarPaint.setColor(mBarColor); invalidate(); } /** * Set the color of the pins. * * @param pinColor Integer specifying the color of the pin. */ public void setPinColor(int pinColor) { setFirstPinColor(pinColor, false); setSecondPinColor(pinColor, true); } public void setFirstPinColor(int pinColor) { setFirstPinColor(pinColor, true); } protected void setFirstPinColor(int pinColor, boolean invalidate) { mFirstPinColor = pinColor; if (invalidate) { initPins(false); } } public void setSecondPinColor(int pinColor) { setSecondPinColor(pinColor, true); } protected void setSecondPinColor(int pinColor, boolean invalidate) { mSecondPinColor = pinColor; if (invalidate) { initPins(false); } } /** * Set the color of the text within the pin. * * @param textColor Integer specifying the color of the text in the pin. */ public void setPinTextColor(int textColor) { setFirstPinTextColor(textColor, false); setSecondPinTextColor(textColor, true); } public void setFirstPinTextColor(int textColor) { setFirstPinTextColor(textColor, true); } protected void setFirstPinTextColor(int textColor, boolean invalidate) { mFirstPinTextColor = textColor; if (invalidate) { initPins(false); } } public void setSecondPinTextColor(int textColor) { setSecondPinTextColor(textColor, true); } protected void setSecondPinTextColor(int textColor, boolean invalidate) { mSecondPinTextColor = textColor; if (invalidate) { initPins(false); } } /** * Gets the type of the bar. * * @return true if rangebar, false if seekbar. */ public boolean isRangeBar() { return mIsRangeBar; } /** * Set if the view is a range bar or a seek bar. * * @param isRangeBar Boolean - true sets it to rangebar, false to seekbar. */ public void setRangeBarEnabled(boolean isRangeBar) { mIsRangeBar = isRangeBar; invalidate(); } /** * Set if the pins should dissapear after released * * @param arePinsTemporary Boolean - true if pins should disappear after released, false to stay * visible */ public void setTemporaryPins(boolean arePinsTemporary) { mArePinsTemporary = arePinsTemporary; invalidate(); } /** * Set the color of the ticks. * * @param tickColor Integer specifying the color of the ticks. */ public void setTickColor(int tickColor) { mTickColor = tickColor; mTickPaint.setColor(mTickColor); invalidate(); } /** * Set the color of the selector. * * @param selectorColor Integer specifying the color of the ticks. */ public void setSelectorColor(int selectorColor) { setFirstSelectorColor(selectorColor, false); setSecondSelectorColor(selectorColor, true); } public void setFirstSelectorColor(int selectorColor) { setFirstSelectorColor(selectorColor, true); } protected void setFirstSelectorColor(int selectorColor, boolean invalidate) { mFirstSelectorColor = selectorColor; if (invalidate) { initPins(false); } } public void setSecondSelectorColor(int selectorColor) { setSecondSelectorColor(selectorColor, true); } protected void setSecondSelectorColor(int selectorColor, boolean invalidate) { mSecondSelectorColor = selectorColor; if (invalidate) { initPins(false); } } /** * Set the weight of the connecting line between the thumbs. * * @param connectingLineWeight float specifying the weight of the connecting * line. */ public void setConnectingLineWeight(float connectingLineWeight) { mConnectingLineWeight = connectingLineWeight; mFirstConnectingLinePaint.setStrokeWidth(mConnectingLineWeight); mSecondConnectingLinePaint.setStrokeWidth(mConnectingLineWeight); invalidate(); } /** * Set the color of the connecting line between the thumbs. * * @param connectingLineColor Integer specifying the color of the connecting * line. */ public void setConnectingLineColor(int connectingLineColor) { setFirstConnectingLineColor(connectingLineColor, true); setSecondConnectingLineColor(connectingLineColor, false); } public void setFirstConnectingLineColor(int connectingLineColor) { setFirstConnectingLineColor(connectingLineColor, true); } protected void setFirstConnectingLineColor(int connectingLineColor, boolean invalidate) { mFirstConnectingLineColor = connectingLineColor; mFirstConnectingLinePaint.setColor(mFirstConnectingLineColor); if (invalidate) { invalidate(); } } public void setSecondConnectingLineColor(int connectingLineColor) { setSecondConnectingLineColor(connectingLineColor, true); } protected void setSecondConnectingLineColor(int connectingLineColor, boolean invalidate) { mSecondConnectingLineColor = connectingLineColor; mSecondConnectingLinePaint.setColor(mSecondConnectingLineColor); if (invalidate) { invalidate(); } } /** * If this is set, the thumb images will be replaced with a circle of the * specified radius. Default width = 20dp. * * @param pinRadius float specifying the radius of the thumbs to be drawn. */ public void setPinRadius(float pinRadius) { mExpandedPinRadius = pinRadius; initPins(false); } /** * Gets the index of the first pin. * * @return the 0-based index of the first pin */ public int getFirstPinIndex() { return mFirstPinView.getIndex(); } /** * Gets the index of the second pin. * * @return the 0-based index of the second pin */ public int getSecondPinIndex() { return mSecondPinView.getIndex(); } public void setPinIndices(int firstPinIndex, int secondPinIndex) { setPinIndices(firstPinIndex, secondPinIndex, null, null); } private boolean setPinIndex(PinView pinView, int index, PointF point) { //PbLog.e(TAG, "setPinIndex(pinView=" + pinView + ", index=" + index + ", point=" + point + ')'); if (pinView == null) { return false; } PointF pointIndex = new PointF(); if (point == null) { getPointOfIndex(index, pointIndex); } else { getNearestPointOnBar(point, pointIndex); } //PbLog.e(TAG, "setPinIndex: pointIndex=" + pointIndex); int indexMin; int indexMax; if (mFirstPinView.equals(pinView)) { indexMin = 0; indexMax = mSecondPinView.getIndex() - 1; } else if (mSecondPinView.equals(pinView)) { indexMin = mFirstPinView.getIndex() + 1; indexMax = mTickCount - 1; } else { //PbLog.e(TAG, "setPinIndex: pinView != mFirstPinView || pinView != mSecondPinView; ignoring"); return false; } //PbLog.e(TAG, "setPinIndex: indexMin=" + indexMin); //PbLog.e(TAG, "setPinIndex: indexMax=" + indexMax); PointF pointMin = new PointF(); getPointOfIndex(indexMin, pointMin); //PbLog.e(TAG, "setPinIndex: pointMin=" + pointMin); PointF pointMax = new PointF(); getPointOfIndex(indexMax, pointMax); //PbLog.e(TAG, "setPinIndex: pointMax=" + pointMax); boolean changed = false; if (comparePointsOnBar(pointMin, pointIndex) > 0) { pointIndex = pointMin; index = indexMin; } else if (comparePointsOnBar(pointIndex, pointMax) > 0) { pointIndex = pointMax; index = indexMax; } else { changed = pinView.setIndex(index); } //PbLog.e(TAG, "setPinIndex: changed=" + changed); movePin(pinView, pointIndex); String label = getPinLabel(index); pinView.setLabel(label); return changed; } private boolean setPinIndices(int firstPinIndex, int secondPinIndex, PinView draggingPin, PointF point) { //PbLog.e(TAG, "setPinIndices(firstPinIndex=" + firstPinIndex + ", secondPinIndex=" + secondPinIndex + ", draggingPin=" + draggingPin + ", point=" + point + ')'); boolean changed = false; //noinspection ConstantConditions changed |= setPinIndex(mSecondPinView, secondPinIndex, mSecondPinView.equals(draggingPin) ? point : null); changed |= setPinIndex(mFirstPinView, firstPinIndex, mFirstPinView.equals(draggingPin) ? point : null); if (changed) { postInvalidate(); boolean fromUser = draggingPin != null; refreshPinIndexes(mFirstPinView.getIndex(), mSecondPinView.getIndex(), fromUser); } return changed; } // // // @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mRefreshData != null) { synchronized (this) { final int count = mRefreshData.size(); for (int i = 0; i < count; i++) { final RefreshData rd = mRefreshData.get(i); doRefreshPinIndexes(rd.firstPinIndex, rd.secondPinIndex, rd.fromUser, true); rd.recycle(); } mRefreshData.clear(); } } mAttached = true; } @Override protected void onDetachedFromWindow() { if (mRefreshProgressRunnable != null) { removeCallbacks(mRefreshProgressRunnable); mRefreshIsPosted = false; } // This should come after stopAnimation(), otherwise an invalidate message remains in the // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation super.onDetachedFromWindow(); mAttached = false; } private class RefreshProgressRunnable implements Runnable { public void run() { synchronized (AbsRangeBar.this) { final int count = mRefreshData.size(); for (int i = 0; i < count; i++) { final RefreshData rd = mRefreshData.get(i); doRefreshPinIndexes(rd.firstPinIndex, rd.secondPinIndex, rd.fromUser, true); rd.recycle(); } mRefreshData.clear(); mRefreshIsPosted = false; } } } private static class RefreshData { private static final int POOL_MAX = 24; private static final Pools.SynchronizedPool<RefreshData> sPool = new Pools.SynchronizedPool<RefreshData>( POOL_MAX); public int firstPinIndex; public int secondPinIndex; public boolean fromUser; public static RefreshData obtain(int firstPinIndex, int secondPinIndex, boolean fromUser) { RefreshData rd = sPool.acquire(); if (rd == null) { rd = new RefreshData(); } rd.firstPinIndex = firstPinIndex; rd.secondPinIndex = secondPinIndex; rd.fromUser = fromUser; return rd; } public void recycle() { sPool.release(this); } } private void refreshPinIndexes(int firstPinIndex, int secondPinIndex, boolean fromUser) { synchronized (AbsRangeBar.this) { if (mUiThreadId == Thread.currentThread().getId()) { doRefreshPinIndexes(firstPinIndex, secondPinIndex, fromUser, true); } else { if (mRefreshProgressRunnable == null) { mRefreshProgressRunnable = new RefreshProgressRunnable(); } final RefreshData rd = RefreshData.obtain(firstPinIndex, secondPinIndex, fromUser); mRefreshData.add(rd); if (mAttached && !mRefreshIsPosted) { post(mRefreshProgressRunnable); mRefreshIsPosted = true; } } } } protected void doRefreshPinIndexes(int firstPinIndex, int secondPinIndex, boolean fromUser, boolean callBackToApp) { synchronized (AbsRangeBar.this) { invalidate(); if (callBackToApp) { onRefreshPinIndexes(firstPinIndex, secondPinIndex, fromUser); } } } protected void onRefreshPinIndexes(int firstPinIndex, int secondPinIndex, boolean fromUser) { setPinIndices(firstPinIndex, secondPinIndex); if (mOnRangeBarChangeListener != null) { mOnRangeBarChangeListener.onRangeChanged(this, mFirstPinView.getIndex(), mSecondPinView.getIndex(), fromUser); } } // // // @Override public void setEnabled(boolean enabled) { if (!enabled) { mBarColor = DEFAULT_BAR_COLOR; mFirstConnectingLineColor = DEFAULT_BAR_COLOR; mSecondConnectingLineColor = DEFAULT_BAR_COLOR; mFirstSelectorColor = DEFAULT_BAR_COLOR; mSecondSelectorColor = DEFAULT_BAR_COLOR; mTickColor = DEFAULT_BAR_COLOR; } else { mBarColor = mActiveBarColor; mFirstConnectingLineColor = mActiveFirstConnectingLineColor; mSecondConnectingLineColor = mActiveSecondConnectingLineColor; mFirstSelectorColor = mActiveFirstCircleColor; mSecondSelectorColor = mActiveSecondCircleColor; mTickColor = mActiveTickColor; } mBarPaint.setColor(mBarColor); mBarPaint.setStrokeWidth(mBarWeight); mTickPaint.setColor(mTickColor); mFirstConnectingLinePaint.setColor(mFirstConnectingLineColor); mFirstConnectingLinePaint.setStrokeWidth(mConnectingLineWeight); mSecondConnectingLinePaint.setColor(mSecondConnectingLineColor); mSecondConnectingLinePaint.setStrokeWidth(mConnectingLineWeight); initPins(false); super.setEnabled(enabled); } public void setConnectingLineInverted(boolean connectingLineInverted) { mConnectingLineInverted = connectingLineInverted; } // Private Methods ///////////////////////////////////////////////////////// /** * Initializes (and creates if necessary) the one or two Pins. */ private void initPins(boolean resetIndexes) { initPin(mFirstPinView, mFirstPinColor, mFirstPinTextColor, mFirstSelectorColor); initPin(mSecondPinView, mSecondPinColor, mSecondPinTextColor, mSecondSelectorColor); if (resetIndexes) { mFirstPinView.setIndex(0); mSecondPinView.setIndex(mTickCount - 1); refreshPinIndexes(mFirstPinView.getIndex(), mSecondPinView.getIndex(), false); } if (!mArePinsTemporary) { pressPin(mFirstPinView); if (mIsRangeBar) { pressPin(mSecondPinView); } } invalidate(); } private void initPin(PinView pinView, int pinColor, int pinTextColor, int pinSelectorColor) { int pinIndex = pinView.getIndex(); PointF pinPoint = new PointF(); getPointOfIndex(pinIndex, pinPoint); pinView.init(pinPoint, 0, pinColor, pinTextColor, mSelectorSize, pinSelectorColor, mMinPinFont, mMaxPinFont); pinView.setLabel(getPinLabel(pinIndex)); } private PinView getTargetPinView(float x, float y) { if (mFirstPinView != null) { if (mFirstPinView.isInTargetZone(x, y)) { return mFirstPinView; } } if (mSecondPinView != null) { if (mSecondPinView.isInTargetZone(x, y)) { return mSecondPinView; } } return null; } /** * Set the thumb to be in the pressed state and calls invalidate() to redraw * the canvas to reflect the updated state. * * @param thumb the thumb to press */ private void pressPin(final PinView thumb) { if (mArePinsTemporary) { ValueAnimator animator = ValueAnimator.ofFloat(0, mExpandedPinRadius); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mPinRadius = (Float) animation.getAnimatedValue(); thumb.setSize(mPinRadius, mPinPadding * animation.getAnimatedFraction()); invalidate(); } }); animator.start(); thumb.press(); } else { thumb.setSize(mExpandedPinRadius, mPinPadding); } } /** * Set the thumb to be in the normal/un-pressed state and calls invalidate() * to redraw the canvas to reflect the updated state. * * @param pinView the thumb to release */ private void releasePin(final PinView pinView) { PointF point = new PointF(); getNearestIndexPosition(pinView.getPosition(), point); pinView.setPosition(point); int tickIndex = getNearestIndex(pinView); pinView.setLabel(getPinLabel(tickIndex)); if (mArePinsTemporary) { ValueAnimator animator = ValueAnimator.ofFloat(mExpandedPinRadius, 0); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mPinRadius = (Float) (animation.getAnimatedValue()); pinView.setSize(mPinRadius, mPinPadding - (mPinPadding * animation.getAnimatedFraction())); invalidate(); } }); animator.start(); pinView.release(); } else { invalidate(); } } /** * Calculates the value for the tickmark at index n. * * @param tickIndex the index to get the value for */ public String getPinLabel(int tickIndex) { if (mIndexFormatter == null) { return Integer.toString(tickIndex); } return mIndexFormatter.getLabel(tickIndex); } /** * Moves the thumb to the given x-coordinate. * * @param pinView the PinView to move * @param point the point to move the PinView to */ private void movePin(PinView pinView, PointF point) { //PbLog.e(TAG, "movePin(pinView=" + pinView + ", point=" + point + ')'); if (pinView.getPosition().equals(point)) { return; } pinView.setPosition(point); invalidate(); } // Bar Implementation /////////////////////////////////////////////////// protected Paint mBarPaint; protected Paint mTickPaint; protected Paint mFirstConnectingLinePaint; protected Paint mSecondConnectingLinePaint; protected void initBar() { // Initialize the paint. mBarPaint = new Paint(); mBarPaint.setAntiAlias(true); mBarPaint.setStyle(Paint.Style.STROKE); mBarPaint.setColor(mBarColor); mBarPaint.setStrokeWidth(mBarWeight); mTickPaint = new Paint(); mTickPaint.setAntiAlias(true); mTickPaint.setColor(mTickColor); // Initialize the paint, set values mFirstConnectingLinePaint = new Paint(); mFirstConnectingLinePaint.setStrokeCap(Paint.Cap.ROUND); mFirstConnectingLinePaint.setStyle(Paint.Style.STROKE); mFirstConnectingLinePaint.setAntiAlias(true); mFirstConnectingLinePaint.setColor(mFirstConnectingLineColor); mFirstConnectingLinePaint.setStrokeWidth(mConnectingLineWeight); mSecondConnectingLinePaint = new Paint(); mSecondConnectingLinePaint.setStrokeCap(Paint.Cap.ROUND); mSecondConnectingLinePaint.setStyle(Paint.Style.STROKE); mSecondConnectingLinePaint.setAntiAlias(true); mSecondConnectingLinePaint.setColor(mSecondConnectingLineColor); mSecondConnectingLinePaint.setStrokeWidth(mConnectingLineWeight); } protected void resizeBar(int w, int h) { // Nothing to do here; Reserved for sublcasses } /** * Draws the tick marks on the bar. * * @param canvas Canvas to draw on; should be the Canvas passed into {#link * View#onDraw()} */ protected void drawTicks(Canvas canvas) { PointF tempPoint = new PointF(); for (int i = 0; i < mTickCount; i++) { getPointOfIndex(i, tempPoint); canvas.drawCircle(tempPoint.x, tempPoint.y, mTickSize, mTickPaint); } } /** * Gets the zero-based index of the nearest tick to the given thumb. * * @param pinView the PinView to find the nearest tick for * @return the zero-based index of the nearest tick */ protected int getNearestIndex(PinView pinView) { return getNearestIndex(pinView.getPosition()); } /** * Gets the x/y-coordinates of the nearest tick to the given point. * * @param pointIn the point of the nearest tick * @param pointOut the nearest tick will be stored in this object */ protected void getNearestIndexPosition(PointF pointIn, PointF pointOut) { final int nearestIndex = getNearestIndex(pointIn); getPointOfIndex(nearestIndex, pointOut); } /** * Draw the connecting line between the two thumbs in RangeBar. * * @param canvas the Canvas to draw to * @param firstPin the first pin * @param secondPin the second pin */ protected void drawConnectingLine(Canvas canvas, PinView firstPin, PinView secondPin) { drawConnectingLine(canvas, firstPin.getPosition(), secondPin.getPosition()); } /** * Compares the two points as they relate to the bar. If point1 * is before point2, result will be <0. If they're at the same point, * 0, and if point1 is after point2, then result will be >0. * * @param point1 * @param point2 * @return */ protected abstract int comparePointsOnBar(PointF point1, PointF point2); /** * Gets the point on the bar nearest to the passed point. * * @param pointIn the point of the nearest point on the bar * @param pointOut the nearest point will be stored in this object */ protected abstract void getNearestPointOnBar(PointF pointIn, PointF pointOut); /** * Gets the zero-based index of the nearest tick to the given point. * * @param point the point to find the nearest tick for * @return the zero-based index of the nearest tick */ protected abstract int getNearestIndex(PointF point); /** * Gets the coordinates of the index-th tick. */ protected abstract void getPointOfIndex(int index, PointF pointOut); /** * Draws the bar on the given Canvas. * * @param canvas Canvas to draw on; should be the Canvas passed into {#link * View#onDraw()} */ protected abstract void drawBar(Canvas canvas); /** * Draw a connecting line between two points that have been precalculated to be on the bar. * * @param canvas * @param firstPoint * @param secondPoint */ protected abstract void drawConnectingLine(Canvas canvas, PointF firstPoint, PointF secondPoint); // Interfaces /////////////////////////////////////////////////////////// /** * A callback that notifies clients when the RangeBar has changed. The * listener will only be called when either thumb's index has changed - not * for every movement of the thumb. */ public interface OnRangeBarChangeListener { /** * Notification that the progress level has changed. Clients can use the fromUser parameter * to distinguish user-initiated changes from those that occurred programmatically. * * @param rangeBar The RangeBar whose progress has changed * @param firstIndex * @param secondIndex * @param fromUser True if the progress change was initiated by the user. */ void onRangeChanged(AbsRangeBar rangeBar, int firstIndex, int secondIndex, boolean fromUser); /** * Notification that the user has started a touch gesture. Clients may want to use this * to disable advancing the RangeBar. * * @param rangeBar The RangeBar in which the touch gesture began */ void onStartTrackingTouch(AbsRangeBar rangeBar); /** * Notification that the user has finished a touch gesture. Clients may want to use this * to re-enable advancing the RangeBar. * * @param rangeBar The RangeBar in which the touch gesture began */ void onStopTrackingTouch(AbsRangeBar rangeBar); } public interface IndexFormatter { String getLabel(int index); } }