Java tutorial
/* * Copyright (C) 2015 AppTik Project * Copyright (C) 2014 Kalin Maldzhanski * * 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 io.apptik.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.util.AttributeSet; import android.view.*; import java.util.LinkedList; public class MultiSlider extends View { public interface OnThumbValueChangeListener { void onValueChanged(MultiSlider multiSlider, MultiSlider.Thumb thumb, int thumbIndex, int value); void onStartTrackingTouch(MultiSlider multiSlider, MultiSlider.Thumb thumb, int value); void onStopTrackingTouch(MultiSlider multiSlider, MultiSlider.Thumb thumb, int value); } private OnThumbValueChangeListener mOnThumbValueChangeListener; int mMinWidth; int mMaxWidth; int mMinHeight; int mMaxHeight; /** * global Min and Max */ private int mScaleMin; private int mScaleMax; private int mStep; private int mStepsThumbsApart; private boolean mDrawThumbsApart; private Drawable mTrack; //used in constructor to prevent invalidating before ready state private boolean mNoInvalidate; private long mUiThreadId; private boolean mInDrawing; private boolean mAttached; private boolean mRefreshIsPosted; boolean mMirrorForRtl = false; //list of all the loaded thumbs private LinkedList<Thumb> mThumbs; /** * Whether this is user seekable. */ boolean mIsUserSeekable = true; /** * On key presses (right or left), the amount to increment/decrement the * progress. */ private int mKeyProgressIncrement = 1; private static final int NO_ALPHA = 0xFF; private float mDisabledAlpha = 0.5f; private int mScaledTouchSlop; private float mTouchDownX; //thumbs that are currently being dragged private LinkedList<Thumb> mDraggingThumbs = new LinkedList<Thumb>(); //thumbs that are currently being touched LinkedList<Thumb> exactTouched = null; private final TypedArray a; public class Thumb { //abs min value for this thumb private int min; //abs max value for this thumb private int max; //current value of this thumb private int value; //thumb drawable, can be shared private Drawable thumb; //thumb range drawable, can also be shared //this is the line from the beginning or the previous thumb if any until the this one. private Drawable range; private int thumbOffset; //cannot be moved if invisible private boolean invisibleThumb = false; public Drawable getRange() { return range; } public Thumb setRange(Drawable range) { this.range = range; return this; } public Thumb() { } public boolean isInvisibleThumb() { return invisibleThumb; } public void setInvisibleThumb(boolean invisibleThumb) { this.invisibleThumb = invisibleThumb; } public int getDrawableValue() { if (thumb == null) return 0; return Math.round(getScaleSize() * thumb.getBounds().width() / getWidth()); } /** * Only useful the keepThumbsApart is set, otherwise return ScaleMin * * @return the minimum value a thumb can obtain depending on other thumbs before it */ public int getPossibleMin() { int res = mScaleMin; res += mThumbs.indexOf(this) * mStepsThumbsApart; return res; } /** * Only useful the keepThumbsApart is set, otherwise return ScaleMax * * @return the maximum value a thumb can have depending the thumbs after it */ public int getPossibleMax() { int res = mScaleMax; res -= (mThumbs.size() - 1 - mThumbs.indexOf(this)) * mStepsThumbsApart; return res; } public int getMin() { return min; } public Thumb setMin(int min) { if (min > this.max) { min = this.max; } if (min < mScaleMin) { min = mScaleMin; } if (this.min != min) { this.min = min; if (value < this.min) { value = this.min; invalidate(); } } return this; } public int getMax() { return max; } public Thumb setMax(int max) { if (max < this.min) { max = this.min; } if (max > mScaleMax) { max = mScaleMax; } if (this.max != max) { this.max = max; if (value > this.max) { value = this.max; invalidate(); } } return this; } public int getValue() { return value; } public Thumb setValue(int value) { setThumbValue(this, value, false); return this; } public Drawable getThumb() { return thumb; } public Thumb setThumb(Drawable mThumb) { this.thumb = mThumb; return this; } public int getThumbOffset() { return thumbOffset; } public Thumb setThumbOffset(int mThumbOffset) { this.thumbOffset = mThumbOffset; return this; } } public MultiSlider(Context context) { this(context, null); } public MultiSlider(Context context, AttributeSet attrs) { this(context, attrs, io.apptik.widget.R.attr.multiSliderStyle); } public MultiSlider(Context context, AttributeSet attrs, int defStyle) { this(context, attrs, defStyle, 0); } public MultiSlider(Context context, AttributeSet attrs, int defStyle, int styleRes) { super(context, attrs, defStyle); mUiThreadId = Thread.currentThread().getId(); a = context.obtainStyledAttributes(attrs, io.apptik.widget.R.styleable.MultiSlider, defStyle, styleRes); mNoInvalidate = true; int numThumbs = a.getInt(io.apptik.widget.R.styleable.MultiSlider_thumbNumber, 2); initMultiSlider(numThumbs); Drawable trackDrawable = a.getDrawable(io.apptik.widget.R.styleable.MultiSlider_android_track); if (trackDrawable == null) { trackDrawable = ContextCompat.getDrawable(getContext(), io.apptik.widget.R.drawable.multislider_scrubber_track_holo_light); } setTrackDrawable(getTintedDrawable(trackDrawable, a.getColor(io.apptik.widget.R.styleable.MultiSlider_trackColor, 0))); //TODO // mMinWidth = a.getDimensionPixelSize(R.styleable.MultiSlider_minWidth, mMinWidth); // mMaxWidth = a.getDimensionPixelSize(R.styleable.MultiSlider_maxWidth, mMaxWidth); // mMinHeight = a.getDimensionPixelSize(R.styleable.MultiSlider_minHeight, mMinHeight); // mMaxHeight = a.getDimensionPixelSize(R.styleable.MultiSlider_maxHeight, mMaxHeight); setStep(a.getInt(io.apptik.widget.R.styleable.MultiSlider_scaleStep, mStep)); setStepsThumbsApart(a.getInt(io.apptik.widget.R.styleable.MultiSlider_stepsThumbsApart, mStepsThumbsApart)); setDrawThumbsApart( a.getBoolean(io.apptik.widget.R.styleable.MultiSlider_drawThumbsApart, mDrawThumbsApart)); setMax(a.getInt(io.apptik.widget.R.styleable.MultiSlider_scaleMax, mScaleMax), true); setMin(a.getInt(io.apptik.widget.R.styleable.MultiSlider_scaleMin, mScaleMin), true); mMirrorForRtl = a.getBoolean(io.apptik.widget.R.styleable.MultiSlider_mirrorForRTL, mMirrorForRtl); // --> now place thumbs Drawable thumbDrawable = a.getDrawable(io.apptik.widget.R.styleable.MultiSlider_android_thumb); if (thumbDrawable == null) { thumbDrawable = ContextCompat.getDrawable(getContext(), io.apptik.widget.R.drawable.multislider_scrubber_control_selector_holo_light); } Drawable range = a.getDrawable(io.apptik.widget.R.styleable.MultiSlider_range); if (range == null) { range = ContextCompat.getDrawable(getContext(), io.apptik.widget.R.drawable.multislider_scrubber_primary_holo); } Drawable range1 = a.getDrawable(io.apptik.widget.R.styleable.MultiSlider_range1); Drawable range2 = a.getDrawable(io.apptik.widget.R.styleable.MultiSlider_range2); setThumbDrawables(thumbDrawable, range, range1, range2); // will guess thumbOffset if thumb != null... // ...but allow layout to override this int thumbOffset = a.getDimensionPixelOffset(io.apptik.widget.R.styleable.MultiSlider_android_thumbOffset, thumbDrawable.getIntrinsicWidth() / 2); setThumbOffset(thumbOffset); positionThumbs(); mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mNoInvalidate = false; a.recycle(); } public int getStepsThumbsApart() { return mStepsThumbsApart; } public void setStepsThumbsApart(int stepsThumbsApart) { if (stepsThumbsApart < 0) stepsThumbsApart = 0; this.mStepsThumbsApart = stepsThumbsApart; } public int getStep() { return mStep; } public void setStep(int mStep) { this.mStep = mStep; } private int getScaleSize() { return mScaleMax - mScaleMin; } private void positionThumbs() { if (mThumbs == null || mThumbs.isEmpty()) return; if (mThumbs.size() > 0) { mThumbs.getFirst().setValue(mScaleMin); } if (mThumbs.size() > 1) { mThumbs.getLast().setValue(mScaleMax); } if (mThumbs.size() > 2) { int even = (mScaleMax - mScaleMin) / (mThumbs.size() - 1); int lastPos = mScaleMax - even; for (int i = mThumbs.size() - 2; i > 0; i--) { mThumbs.get(i).setValue(lastPos); lastPos -= even; } } } public void setOnThumbValueChangeListener(OnThumbValueChangeListener l) { mOnThumbValueChangeListener = l; } public boolean isDrawThumbsApart() { return mDrawThumbsApart; } public void setDrawThumbsApart(boolean drawThumbsApart) { this.mDrawThumbsApart = drawThumbsApart; } private void initMultiSlider(int numThumbs) { mStep = 1; mStepsThumbsApart = 0; mDrawThumbsApart = false; mScaleMin = 0; mScaleMax = 100; mMinWidth = 24; mMaxWidth = 48; mMinHeight = 24; mMaxHeight = 48; mThumbs = new LinkedList<Thumb>(); for (int i = 0; i < numThumbs; i++) { mThumbs.add(new Thumb().setMin(mScaleMin).setMax(mScaleMax)); } } public void setThumbOffset(int thumbOffset) { for (Thumb thumb : mThumbs) { thumb.setThumbOffset(thumbOffset); } invalidate(); } public void setTrackDrawable(Drawable d) { boolean needUpdate; if (mTrack != null && d != mTrack) { mTrack.setCallback(null); needUpdate = true; } else { needUpdate = false; } if (d != null) { d.setCallback(this); // if (canResolveLayoutDirection()) { // d.setLayoutDirection(getLayoutDirection()); // } // Make sure the ProgressBar is always tall enough int drawableHeight = d.getMinimumHeight(); if (mMaxHeight < drawableHeight) { mMaxHeight = drawableHeight; requestLayout(); } } mTrack = d; if (needUpdate) { updateTrackBounds(getWidth(), getHeight()); updateTrackState(); //TODO update all thumbs with their range tracks also } } private int optThumbValue(Thumb thumb, int value) { if (thumb == null || thumb.getThumb() == null) return value; int currIdx = mThumbs.indexOf(thumb); if (mThumbs.size() > currIdx + 1 && value > mThumbs.get(currIdx + 1).getValue() - mStepsThumbsApart * mStep) { value = mThumbs.get(currIdx + 1).getValue() - mStepsThumbsApart * mStep; } if (currIdx > 0 && value < mThumbs.get(currIdx - 1).getValue() + mStepsThumbsApart * mStep) { value = mThumbs.get(currIdx - 1).getValue() + mStepsThumbsApart * mStep; } if ((value - mScaleMin) % mStep != 0) { value += mStep - ((value - mScaleMin) % mStep); } if (value < thumb.getMin()) { value = thumb.getMin(); } if (value > thumb.getMax()) { value = thumb.getMax(); } return value; } /** * Refreshes the value for the specific thumb * * @param thumb the thumb which value is going to be changed * @param value the new value * @param fromUser if the request is coming form the user or the client */ private synchronized void setThumbValue(Thumb thumb, int value, boolean fromUser) { if (thumb == null || thumb.getThumb() == null) return; value = optThumbValue(thumb, value); if (value != thumb.getValue()) { thumb.value = value; } if (hasOnThumbValueChangeListener()) { mOnThumbValueChangeListener.onValueChanged(this, thumb, mThumbs.indexOf(thumb), thumb.getValue()); } updateThumb(thumb, getWidth(), getHeight()); } private synchronized void setThumbValue(int thumb, int value, boolean fromUser) { setThumbValue(mThumbs.get(thumb), value, fromUser); } private void updateTrackBounds(int w, int h) { // onDraw will translate the canvas so we draw starting at 0,0. // Subtract out padding for the purposes of the calculations below. w -= getPaddingRight() + getPaddingLeft(); h -= getPaddingTop() + getPaddingBottom(); int right = w; int bottom = h; int top = 0; int left = 0; if (mTrack != null) { mTrack.setBounds(0, 0, right, bottom); } } private void updateTrackState() { int[] state = getDrawableState(); if (mTrack != null && mTrack.isStateful()) { mTrack.setState(state); } } /** * Sets the thumb drawable for all thumbs * <p/> * If the thumb is a valid drawable (i.e. not null), half its width will be * used as the new thumb offset (@see #setThumbOffset(int)). * * @param thumb Drawable representing the thumb */ private void setThumbDrawables(Drawable thumb, Drawable range, Drawable range1, Drawable range2) { if (thumb == null) return; boolean needUpdate; Drawable rangeDrawable; // This way, calling setThumbDrawables again with the same bitmap will result in // it recalculating thumbOffset (if for example it the bounds of the // drawable changed) int curr = 0; int padding = 0; for (Thumb mThumb : mThumbs) { curr++; if (mThumb.getThumb() != null && thumb != mThumb.getThumb()) { mThumb.getThumb().setCallback(null); needUpdate = true; } else { needUpdate = false; } if (curr == 1 && range1 != null) { rangeDrawable = getTintedDrawable(range1, a.getColor(io.apptik.widget.R.styleable.MultiSlider_range1Color, 0)); } else if (curr == 2 && range2 != null) { rangeDrawable = getTintedDrawable(range2, a.getColor(io.apptik.widget.R.styleable.MultiSlider_range2Color, 0)); } else { rangeDrawable = getTintedDrawable(range.getConstantState().newDrawable(), a.getColor(io.apptik.widget.R.styleable.MultiSlider_rangeColor, 0)); } mThumb.setRange(rangeDrawable); Drawable newDrawable = getTintedDrawable(thumb.getConstantState().newDrawable(), a.getColor(io.apptik.widget.R.styleable.MultiSlider_thumbColor, 0)); newDrawable.setCallback(this); // Assuming the thumb drawable is symmetric, set the thumb offset // such that the thumb will hang halfway off either edge of the // progress bar. mThumb.setThumbOffset(thumb.getIntrinsicWidth() / 2); padding = Math.max(padding, mThumb.getThumbOffset()); // If we're updating get the new states if (needUpdate && (newDrawable.getIntrinsicWidth() != mThumb.getThumb().getIntrinsicWidth() || newDrawable.getIntrinsicHeight() != mThumb.getThumb().getIntrinsicHeight())) { requestLayout(); } mThumb.setThumb(newDrawable); if (needUpdate) { invalidate(); if (thumb != null && thumb.isStateful()) { // Note that if the states are different this won't work. // For now, let's consider that an app bug. int[] state = getDrawableState(); thumb.setState(state); } } } setPadding(padding, getPaddingTop(), padding, getPaddingBottom()); } /** * Return the drawable used to represent the scroll thumb - the component that * the user can drag back and forth indicating the current value by its position. * * @return The thumb at position pos */ public Thumb getThumb(int pos) { return mThumbs.get(pos); } /** * Sets the amount of progress changed via the arrow keys. * * @param increment The amount to increment or decrement when the user * presses the arrow keys. */ public void setKeyProgressIncrement(int increment) { mKeyProgressIncrement = increment < 0 ? -increment : increment; } /** * Returns the amount of progress changed via the arrow keys. * <p/> * By default, this will be a value that is derived from the max progress. * * @return The amount to increment or decrement when the user presses the * arrow keys. This will be positive. */ public int getKeyProgressIncrement() { return mKeyProgressIncrement; } public synchronized void setMax(int max) { setMax(max, true, false); } public synchronized void setMax(int max, boolean extendMaxForThumbs) { setMax(max, extendMaxForThumbs, false); } public synchronized void setMax(int max, boolean extendMaxForThumbs, boolean repositionThumbs) { if (max < mScaleMin) { max = mScaleMin; } if (max != mScaleMax) { mScaleMax = max; //check for thumbs out of bounds and adjust the max for those exceeding the new one for (Thumb thumb : mThumbs) { if (extendMaxForThumbs) { thumb.setMax(max); } else if (thumb.getMax() > max) { thumb.setMax(max); } if (thumb.getValue() > max) { setThumbValue(thumb, max, false); } } if (repositionThumbs) positionThumbs(); postInvalidate(); } if ((mKeyProgressIncrement == 0) || (mScaleMax / mKeyProgressIncrement > 20)) { // It will take the user too long to change this via keys, change it // to something more reasonable setKeyProgressIncrement(Math.max(1, Math.round((float) mScaleMax / 20))); } } public synchronized void setMin(int min) { setMin(min, true, false); } public synchronized void setMin(int min, boolean extendMaxForThumbs) { setMin(min, extendMaxForThumbs, false); } public synchronized void setMin(int min, boolean extendMaxForThumbs, boolean repositionThumbs) { if (min > mScaleMax) { min = mScaleMax; } if (min != mScaleMin) { mScaleMin = min; //check for thumbs out of bounds and adjust the max for those exceeding the new one for (Thumb thumb : mThumbs) { if (extendMaxForThumbs) { thumb.setMin(min); } else if (thumb.getMin() < min) { thumb.setMin(min); } if (thumb.getValue() < min) { setThumbValue(thumb, min, false); } } if (repositionThumbs) positionThumbs(); postInvalidate(); } if ((mKeyProgressIncrement == 0) || (mScaleMax / mKeyProgressIncrement > 20)) { // It will take the user too long to change this via keys, change it // to something more reasonable setKeyProgressIncrement(Math.max(1, Math.round((float) mScaleMax / 20))); } } @Override protected boolean verifyDrawable(Drawable who) { for (Thumb thumb : mThumbs) { if (thumb.getThumb() != null && who == thumb.getThumb()) return true; } return who == mTrack || super.verifyDrawable(who); } @Override public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); for (Thumb thumb : mThumbs) { if (thumb.getThumb() != null) thumb.getThumb().jumpToCurrentState(); } } @Override protected void drawableStateChanged() { super.drawableStateChanged(); if (mDraggingThumbs != null && !mDraggingThumbs.isEmpty()) { int[] state = getDrawableState(); for (Thumb thumb : mDraggingThumbs) { if (thumb.getThumb() != null) thumb.getThumb().setState(state); } for (Thumb thumb : mThumbs) { if (!mDraggingThumbs.contains(thumb) && thumb.getThumb() != null && thumb.getThumb().isStateful()) { thumb.getThumb().setState(new int[] { android.R.attr.state_enabled }); } } } else { int[] state = getDrawableState(); for (Thumb thumb : mThumbs) { if (thumb.getThumb() != null && thumb.getThumb().isStateful()) { thumb.getThumb().setState(state); } } } } /** * Updates Thumb drawable position according to the new w,h * * @param thumb the thumb object * @param w width * @param h height */ private void updateThumb(Thumb thumb, int w, int h) { int thumbHeight = thumb == null ? 0 : thumb.getThumb().getIntrinsicHeight(); // The max height does not incorporate padding, whereas the height // parameter does int trackHeight = h - getPaddingTop() - getPaddingBottom(); float scale = getScaleSize() > 0 ? (float) thumb.getValue() / (float) getScaleSize() : 0; Drawable prevThumb = null; int currIdx = mThumbs.indexOf(thumb); if (currIdx > 0) { prevThumb = mThumbs.get(currIdx - 1).getThumb(); } if (thumbHeight > trackHeight) { if (thumb != null) { setThumbPos(w, h, thumb.getThumb(), prevThumb, thumb.getRange(), scale, 0, thumb.getThumbOffset(), getThumbOptOffset(thumb)); } int gapForCenteringTrack = (thumbHeight - trackHeight) / 2; if (mTrack != null) { // Canvas will be translated by the padding, so 0,0 is where we start drawing mTrack.setBounds(0, gapForCenteringTrack, w - getPaddingRight() - getPaddingLeft(), h - getPaddingBottom() - gapForCenteringTrack - getPaddingTop()); } } else { if (mTrack != null) { // Canvas will be translated by the padding, so 0,0 is where we start drawing mTrack.setBounds(0, 0, w - getPaddingRight() - getPaddingLeft(), h - getPaddingBottom() - getPaddingTop()); } int gap = (trackHeight - thumbHeight) / 2; if (thumb != null) { setThumbPos(w, h, thumb.getThumb(), prevThumb, thumb.getRange(), scale, gap, thumb.getThumbOffset(), getThumbOptOffset(thumb)); } } //update thumbs after it for (int i = currIdx + 1; i < mThumbs.size(); i++) { int gap = (trackHeight - thumbHeight) / 2; scale = getScaleSize() > 0 ? (float) mThumbs.get(i).getValue() / (float) getScaleSize() : 0; setThumbPos(w, h, mThumbs.get(i).getThumb(), mThumbs.get(i - 1).getThumb(), mThumbs.get(i).getRange(), scale, gap, mThumbs.get(i).getThumbOffset(), getThumbOptOffset(mThumbs.get(i))); } } /** * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and */ private void setThumbPos(int w, int h, Drawable thumb, Drawable prevThumb, Drawable range, float scale, int gap, int thumbOffset, int optThumbOffset) { final int available = getAvailable(); int thumbWidth = thumb.getIntrinsicWidth(); int thumbHeight = thumb.getIntrinsicHeight(); //todo change available before also float scaleOffset = getScaleSize() > 0 ? (float) mScaleMin / (float) getScaleSize() : 0; int thumbPos = (int) (scale * available - scaleOffset * available + 0.5f); int topBound, bottomBound; if (gap == Integer.MIN_VALUE) { Rect oldBounds = thumb.getBounds(); topBound = oldBounds.top; bottomBound = oldBounds.bottom; } else { topBound = gap; bottomBound = gap + thumbHeight; } // Canvas will be translated, so 0,0 is where we start drawing final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos - optThumbOffset : thumbPos + optThumbOffset; thumb.setBounds(left, topBound, left + thumbWidth, bottomBound); w -= getPaddingRight() + getPaddingLeft(); h -= getPaddingTop() + getPaddingBottom(); int right = w; int bottom = h; int leftRange = 0; if (prevThumb != null) { leftRange = prevThumb.getBounds().left; } if (range != null) { range.setBounds(leftRange, 0, left, bottom); } invalidate(); } @Override protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); // --> draw track if (mTrack != null) { // Translate canvas so a indeterminate circular progress bar with padding // rotates properly in its animation canvas.save(); if (isLayoutRtl() && mMirrorForRtl) { canvas.translate(getWidth() - getPaddingRight(), getPaddingTop()); canvas.scale(-1.0f, 1.0f); } else { canvas.translate(getPaddingLeft(), getPaddingTop()); } mTrack.draw(canvas); canvas.restore(); } // --> draw ranges for (Thumb thumb : mThumbs) { if (thumb.getRange() != null) { canvas.save(); if (isLayoutRtl() && mMirrorForRtl) { canvas.translate(getWidth() - getPaddingRight(), getPaddingTop()); canvas.scale(-1.0f, 1.0f); } else { canvas.translate(getPaddingLeft(), getPaddingTop()); } thumb.getRange().draw(canvas); canvas.restore(); } } // --> then draw thumbs for (Thumb thumb : mThumbs) { if (thumb.getThumb() != null && !thumb.isInvisibleThumb()) { canvas.save(); // Translate the padding. For the x, we need to allow the thumb to // draw in its extra space canvas.translate(getPaddingLeft() - thumb.getThumbOffset(), getPaddingTop()); // float scale = mScaleMax > 0 ? (float) thumb.getValue() / (float) mScaleMax : 0; thumb.getThumb().draw(canvas); canvas.restore(); } } } @Override protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int maxThumbHeight = 0; int maxRangeHeight = 0; for (Thumb thumb : mThumbs) { if (thumb.getThumb() != null) { maxThumbHeight = Math.max(thumb.getThumb().getIntrinsicHeight(), maxThumbHeight); maxRangeHeight = Math.max(thumb.getThumb().getIntrinsicHeight(), maxRangeHeight); } } int dw = 0; int dh = 0; if (mTrack != null) { dw = Math.max(mMinWidth, Math.min(mMaxWidth, mTrack.getIntrinsicWidth())); dh = Math.max(mMinHeight, Math.min(mMaxHeight, mTrack.getIntrinsicHeight())); dh = Math.max(maxRangeHeight, dh); dh = Math.max(maxThumbHeight, dh); } dw += getPaddingLeft() + getPaddingRight(); dh += getPaddingTop() + getPaddingBottom(); setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0), resolveSizeAndState(dh, heightMeasureSpec, 0)); } public boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; } private int getAvailable() { int available = getWidth() - getPaddingLeft() - getPaddingRight(); if (mThumbs != null && mThumbs.size() > 0) { available -= getThumbOptOffset(mThumbs.getLast()); } //TODO check for the offset return available; } /** * Get closest thumb to play with, * incase more than one get the last one * * @param x X coordinate of the touch * @return */ private LinkedList<Thumb> getClosestThumb(int x) { LinkedList<Thumb> exact = new LinkedList<Thumb>(); Thumb closest = null; int currDistance = getAvailable() + 1; for (Thumb thumb : mThumbs) { if (thumb.getThumb() == null || thumb.isInvisibleThumb()) continue; int minV = x - thumb.getThumb().getIntrinsicWidth(); int maxV = x + thumb.getThumb().getIntrinsicWidth(); if (thumb.getThumb().getBounds().centerX() >= minV && thumb.getThumb().getBounds().centerX() <= maxV) { //we have exact match // we add them all so we can choose later which one to move exact.add(thumb); } else if (Math.abs(thumb.getThumb().getBounds().centerX() - x) <= currDistance) { if (Math.abs(thumb.getThumb().getBounds().centerX() - x) == currDistance) { if (x > getWidth() / 2) { //left one(s) has more place to move closest = thumb; } else { //right one(s) has more place to move } } else { if (thumb.getThumb() != null) { currDistance = Math.abs(thumb.getThumb().getBounds().centerX() - x); closest = thumb; } } } } if (exact.isEmpty() && closest != null) { exact.add(closest); } return exact; } private Thumb getMostMovable(LinkedList<Thumb> thumbs, MotionEvent event) { Thumb res = null; int maxChange = 0; if (thumbs != null && !thumbs.isEmpty()) { if (thumbs.getFirst().getValue() == getValue(event, thumbs.getFirst())) return null; for (Thumb thumb : thumbs) { int optValue = (getValue(event, thumbs.getFirst()) > thumb.getValue()) ? mScaleMax : mScaleMin; int currChange = Math.abs(thumb.getValue() - optThumbValue(thumb, optValue)); if (currChange > maxChange) { maxChange = currChange; res = thumb; } } } return res; } @Override public boolean onTouchEvent(MotionEvent event) { if (!mIsUserSeekable || !isEnabled()) { return false; } int pointerIdx = event.getActionIndex(); Thumb currThumb = null; if (mDraggingThumbs.size() > pointerIdx) { currThumb = mDraggingThumbs.get(pointerIdx); } else { LinkedList<Thumb> closestOnes = getClosestThumb((int) event.getX(event.getActionIndex())); if (closestOnes != null && !closestOnes.isEmpty()) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN || event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { if (closestOnes.size() == 1) { currThumb = closestOnes.getFirst(); onStartTrackingTouch(currThumb); drawableStateChanged(); } else { //we have more than one thumb at the same place and we touched there exactTouched = closestOnes; } } else if (exactTouched != null && !exactTouched.isEmpty() && event.getActionMasked() == MotionEvent.ACTION_MOVE) { //we have thumbs waiting to be selected to move currThumb = getMostMovable(exactTouched, event); //check if move actually changed value if (currThumb == null) return false; exactTouched = null; onStartTrackingTouch(currThumb); drawableStateChanged(); } else { currThumb = closestOnes.getFirst(); onStartTrackingTouch(currThumb); drawableStateChanged(); } } } switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: if (isInScrollingContainer()) { mTouchDownX = event.getX(); } else { //currThumb = getClosestThumb(newValue); //onStartTrackingTouch(currThumb); setPressed(true); if (currThumb != null && currThumb.getThumb() != null) { invalidate(currThumb.getThumb().getBounds()); // This may be within the padding region } int value = getValue(event, currThumb); setThumbValue(currThumb, value, true); attemptClaimDrag(); } break; case MotionEvent.ACTION_POINTER_DOWN: if (isInScrollingContainer()) { mTouchDownX = event.getX(); } else { //currThumb = getClosestThumb(newValue); //onStartTrackingTouch(currThumb); setPressed(true); if (currThumb != null && currThumb.getThumb() != null) { invalidate(currThumb.getThumb().getBounds()); // This may be within the padding region } setThumbValue(currThumb, getValue(event, currThumb), true); attemptClaimDrag(); } invalidate(); break; //with move we dont have pointer action so set them all case MotionEvent.ACTION_MOVE: if (!mDraggingThumbs.isEmpty()) { //need the index for (int i = 0; i < mDraggingThumbs.size(); i++) { setPressed(true); if (mDraggingThumbs.get(i) != null && mDraggingThumbs.get(i).getThumb() != null) { invalidate(mDraggingThumbs.get(i).getThumb().getBounds()); // This may be within the padding region } setThumbValue(mDraggingThumbs.get(i), getValue(event, i, mDraggingThumbs.get(i)), true); attemptClaimDrag(); } } else { final float x = event.getX(); if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) { //currThumb = getClosestThumb(newValue); //onStartTrackingTouch(currThumb); setPressed(true); if (currThumb != null && currThumb.getThumb() != null) { invalidate(currThumb.getThumb().getBounds()); // This may be within the padding region } setThumbValue(currThumb, getValue(event, currThumb), true); attemptClaimDrag(); } } break; //there are other pointers left case MotionEvent.ACTION_POINTER_UP: if (currThumb != null) { setThumbValue(currThumb, getValue(event, currThumb), true); onStopTrackingTouch(currThumb); } else { // currThumb = getClosestThumb(newValue); // // Touch up when we never crossed the touch slop threshold should // // be interpreted as a tap-seek to that location. // onStartTrackingTouch(currThumb); // setThumbValue(currThumb, newValue, true); // onStopTrackingTouch(currThumb); } // 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; //we normally have one single pointer here and its gone now case MotionEvent.ACTION_UP: if (currThumb != null) { int value = getValue(event, currThumb); setThumbValue(currThumb, value, true); onStopTrackingTouch(currThumb); } else { // currThumb = getClosestThumb(newValue); // // Touch up when we never crossed the touch slop threshold should // // be interpreted as a tap-seek to that location. // onStartTrackingTouch(currThumb); // setThumbValue(currThumb, newValue, true); // onStopTrackingTouch(); } setPressed(false); // 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: if (mDraggingThumbs != null) { onStopTrackingTouch(); setPressed(false); } invalidate(); // see above explanation break; } return true; } private int getValue(MotionEvent event, Thumb thumb) { return getValue(event, event.getActionIndex(), thumb); } int getThumbOptOffset(Thumb thumb) { if (!mDrawThumbsApart) return 0; if (thumb == null || thumb.getThumb() == null) return 0; int thumbIdx = mThumbs.indexOf(thumb); if (isLayoutRtl()) { return (thumbIdx == mThumbs.size() - 1) ? 0 : (getThumbOptOffset(mThumbs.get(thumbIdx + 1)) + thumb.getThumb().getIntrinsicWidth()); } else { return (thumbIdx == 0) ? 0 : (getThumbOptOffset(mThumbs.get(thumbIdx - 1)) + thumb.getThumb().getIntrinsicWidth()); } } private int getValue(MotionEvent event, int pointerIndex, Thumb thumb) { final int width = getWidth(); final int available = getAvailable(); int optThumbOffset = getThumbOptOffset(thumb); int x = (int) event.getX(pointerIndex); float scale; float progress = mScaleMin; if (isLayoutRtl()) { if (x > width - getPaddingRight()) { scale = 0.0f; } else if (x < getPaddingLeft()) { scale = 1.0f; } else { scale = (float) (available - x + getPaddingLeft() + optThumbOffset) / (float) available; progress = mScaleMin; } } else { if (x < getPaddingLeft()) { scale = 0.0f; } else if (x > width - getPaddingRight()) { scale = 1.0f; } else { scale = (float) (x - getPaddingLeft() - optThumbOffset) / (float) available; progress = mScaleMin; } } progress += scale * getScaleSize(); return Math.round(progress); } /** * Tries to claim the user's drag motion, and requests disallowing any * ancestors from stealing events in the drag. */ private void attemptClaimDrag() { if (getParent() != null) { getParent().requestDisallowInterceptTouchEvent(true); } } /** * This is called when the user has started touching this widget. */ void onStartTrackingTouch(Thumb thumb) { if (thumb != null) { if (hasOnThumbValueChangeListener()) { mOnThumbValueChangeListener.onStartTrackingTouch(this, thumb, thumb.getValue()); } mDraggingThumbs.add(thumb); } } /** * This is called when the user either releases his touch or the touch is * canceled. */ void onStopTrackingTouch(Thumb thumb) { if (thumb != null) { mDraggingThumbs.remove(thumb); if (hasOnThumbValueChangeListener()) { mOnThumbValueChangeListener.onStopTrackingTouch(this, thumb, thumb.getValue()); } } drawableStateChanged(); } void onStopTrackingTouch() { mDraggingThumbs.clear(); } private boolean hasOnThumbValueChangeListener() { return mOnThumbValueChangeListener != null; } // void onKeyChange() { // } // // @Override // public boolean onKeyDown(int keyCode, KeyEvent event) { // if (isEnabled()) { // int progress = getProgress(); // switch (keyCode) { // case KeyEvent.KEYCODE_DPAD_LEFT: // if (progress <= 0) break; // //setProgress(progress - mKeyProgressIncrement, true); // onKeyChange(); // return true; // // case KeyEvent.KEYCODE_DPAD_RIGHT: // if (progress >= getMax()) break; // //setProgress(progress + mKeyProgressIncrement, true); // onKeyChange(); // return true; // } // } // // return super.onKeyDown(keyCode, event); // } // @Override // public void onInitializeAccessibilityEvent(AccessibilityEvent event) { // super.onInitializeAccessibilityEvent(event); // event.setClassName(MultiSlider.class.getName()); // } // // @Override // public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { // super.onInitializeAccessibilityNodeInfo(info); // info.setClassName(MultiSlider.class.getName()); // // if (isEnabled()) { // final int progress = getProgress(); // if (progress > 0) { // info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); // } // if (progress < getMax()) { // info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); // } // } // } //// // @Override // public boolean performAccessibilityAction(int action, Bundle arguments) { // if(Build.VERSION.SDK_INT>=16) { // if (super.performAccessibilityAction(action, arguments)) { // return true; // } // } // if (!isEnabled()) { // return false; // } // final int progress = getProgress(); // final int increment = Math.max(1, Math.round((float) getMax() / 5)); // switch (action) { // case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { // if (progress <= 0) { // return false; // } // //setProgress(progress - increment, true); // onKeyChange(); // return true; // } // case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { // if (progress >= getMax()) { // return false; // } // //setProgress(progress + increment, true); // onKeyChange(); // return true; // } // } // return false; // } // @Override // public void onRtlPropertiesChanged(int layoutDirection) { // if(Build.VERSION.SDK_INT>=17){ // super.onRtlPropertiesChanged(layoutDirection); // } // // int max = getMax(); // float scale = max > 0 ? (float) getProgress() / (float) max : 0; // // Drawable thumb = mThumb; // if (thumb != null) { // setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); // /* // * Since we draw translated, the drawable's bounds that it signals // * for invalidation won't be the actual bounds we want invalidated, // * so just invalidate this whole view. // */ // invalidate(); // } // } public boolean isLayoutRtl() { if (Build.VERSION.SDK_INT >= 17) { return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); } return false; } @Override public void invalidateDrawable(Drawable dr) { if (!mInDrawing) { if (verifyDrawable(dr)) { final Rect dirty = dr.getBounds(); final int scrollX = getScrollX() + getPaddingLeft(); final int scrollY = getScrollY() + getPaddingTop(); invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY); } else { super.invalidateDrawable(dr); } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { updateTrackBounds(w, h); for (Thumb thumb : mThumbs) { updateThumb(thumb, w, h); } } private Drawable getTintedDrawable(Drawable drawable, int tintColor) { if (drawable != null && tintColor != 0) { Drawable wrappedDrawable = DrawableCompat.wrap(drawable.mutate()); DrawableCompat.setTint(wrappedDrawable, tintColor); return wrappedDrawable; } return drawable; } public static class SimpleOnThumbValueChangeListener implements OnThumbValueChangeListener { @Override public void onValueChanged(MultiSlider multiSlider, Thumb thumb, int thumbIndex, int value) { } @Override public void onStartTrackingTouch(MultiSlider multiSlider, Thumb thumb, int value) { } @Override public void onStopTrackingTouch(MultiSlider multiSlider, Thumb thumb, int value) { } } }