/* * 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.os.Bundle; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.text.TextUtils; 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 android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import io.apptik.widget.mslider.R; import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD; import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD; import static io.apptik.widget.Util.requireNonNull; public class MultiSlider extends View { public interface OnThumbValueChangeListener { /** * called when thumb value has changed * * @param multiSlider * @param thumb the thumb which values has changes * @param thumbIndex the index of the thumb * @param value the value that has been set */ void onValueChanged(MultiSlider multiSlider, MultiSlider.Thumb thumb, int thumbIndex, int value); } public interface OnTrackingChangeListener { /** * This is called when the user has started touching this widget. * * @param multiSlider * @param thumb the thumb that has been selected * @param value the initial value of the thumb before any movement */ void onStartTrackingTouch(MultiSlider multiSlider, MultiSlider.Thumb thumb, int value); /** * This is called when the user either releases his touch or the touch is canceled. * * @param multiSlider * @param thumb the thumb that has been selected * @param value the last and remaining value of the thumb after the move completes */ void onStopTrackingTouch(MultiSlider multiSlider, MultiSlider.Thumb thumb, int value); } private AccessibilityNodeProvider mAccessibilityNodeProvider; private OnThumbValueChangeListener mOnThumbValueChangeListener; private OnTrackingChangeListener mOnTrackingChangeListener; 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 = true; //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 List<Thumb> mDraggingThumbs = new LinkedList<>(); //thumbs that are currently being touched LinkedList<Thumb> exactTouched = null; private Drawable defThumbDrawable; private int defThumbColor = 0; private Drawable defRangeDrawable; private int defRangeColor = 0; private final TypedArray a; /** * Thumb is the main object in MultiSlider. * There could be 0, 1 or many thumbs. Each thumb has a min and max limit and a value which * should always be between the limits. Each thumb defines a 'Range' the range is always * between the value of the Thumb back to the previous Thumb's value or to the beginning of * the track. */ public class Thumb { //abs min value for this thumb int min; //abs max value for this thumb int max; //current value of this thumb int value; //thumb tag. can be used for identifying the thumb String tag = "thumb"; //thumb drawable, can be shared 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. Drawable range; int thumbOffset; //cannot be moved if invisible and it is not displayed private boolean isInvisible = false; //cannot be moved if not enabled private boolean isEnabled = true; public Thumb() { min = mScaleMin; max = mScaleMax; value = max; } /** * @return the range drawable */ public Drawable getRange() { return range; } /** * Set the range drawable * * @param range * @return */ public final Thumb setRange(Drawable range) { this.range = range; return this; } public boolean isEnabled() { return !isInvisibleThumb() && isEnabled; } public Thumb setEnabled(boolean enabled) { isEnabled = enabled; if (getThumb() != null) { if (isEnabled()) { getThumb().setState(new int[]{android.R.attr.state_enabled}); } else { getThumb().setState(new int[]{-android.R.attr.state_enabled}); } } return this; } /** * @return true is the thumb is invisible, false otherwise */ public boolean isInvisibleThumb() { return isInvisible; } /** * Sets thumb's visibility * * @param invisibleThumb */ public void setInvisibleThumb(boolean invisibleThumb) { this.isInvisible = invisibleThumb; } /** * @return the minimum value a thumb can obtain depending on other thumbs before it */ public int getPossibleMin() { int res = min; res += mThumbs.indexOf(this) * mStepsThumbsApart; return res; } /** * @return the maximum value a thumb can have depending the thumbs after it */ public int getPossibleMax() { int res = max; res -= (mThumbs.size() - 1 - mThumbs.indexOf(this)) * mStepsThumbsApart; return res; } /** * @return the minimum value a thumb can have regardless of the thumbs after it */ public int getMin() { return min; } /** * @param min the minimum value a thumb can have * @return */ 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; } /** * @return the maximum value a thumb can have regardless of the thumbs after it */ public int getMax() { return max; } /** * @param max he maximum value a thumb can have * @return */ 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; } /** * @return Thumb's current value */ public int getValue() { return value; } /** * Manually set a thumb value * * @param value * @return */ public Thumb setValue(int value) { if(mThumbs.contains(this)) { setThumbValue(this, value, false); } else { this.value = value; } return this; } public String getTag() { return tag; } public Thumb setTag(String tag) { this.tag = tag; return this; } /** * @return The thumb drawable */ public Drawable getThumb() { return thumb; } /** * @param mThumb the thumb drawable * @return */ public Thumb setThumb(Drawable mThumb) { this.thumb = mThumb; return this; } /** * @return thumb offset in pixels */ public int getThumbOffset() { return thumbOffset; } /** * @param mThumbOffset thumb offset in pixels * @return */ 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.mslider.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); if ((Build.VERSION.SDK_INT >= 21) && getBackground() == null) { setBackgroundResource(R.drawable.control_background_multi_material); } mUiThreadId = Thread.currentThread().getId(); a = context.obtainStyledAttributes(attrs, io.apptik.widget.mslider.R.styleable.MultiSlider, defStyle, styleRes); mNoInvalidate = true; int numThumbs = a.getInt(io.apptik.widget.mslider.R.styleable.MultiSlider_thumbNumber, 2); initMultiSlider(numThumbs); Drawable trackDrawable = a.getDrawable(io.apptik.widget.mslider.R.styleable .MultiSlider_android_track); if (trackDrawable == null) { trackDrawable = ContextCompat.getDrawable(getContext(), R.drawable.multislider_track_material ); } setTrackDrawable(getTintedDrawable(trackDrawable, a.getColor(io.apptik.widget.mslider.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.mslider.R.styleable.MultiSlider_scaleStep, mStep)); setStepsThumbsApart(a.getInt(io.apptik.widget.mslider.R.styleable .MultiSlider_stepsThumbsApart, mStepsThumbsApart)); setDrawThumbsApart(a.getBoolean(io.apptik.widget.mslider.R.styleable .MultiSlider_drawThumbsApart, mDrawThumbsApart)); setMax(a.getInt(io.apptik.widget.mslider.R.styleable.MultiSlider_scaleMax, mScaleMax), true); setMin(a.getInt(io.apptik.widget.mslider.R.styleable.MultiSlider_scaleMin, mScaleMin), true); mMirrorForRtl = a.getBoolean(io.apptik.widget.mslider.R.styleable.MultiSlider_mirrorForRTL, mMirrorForRtl); // --> now place thumbs defThumbDrawable = a.getDrawable(io.apptik.widget.mslider.R.styleable .MultiSlider_android_thumb); if (defThumbDrawable == null) { if (Build.VERSION.SDK_INT >= 21) { defThumbDrawable = ContextCompat.getDrawable(getContext(), R.drawable .multislider_thumb_material_anim); } else { defThumbDrawable = ContextCompat.getDrawable(getContext(), R.drawable .multislider_thumb_material); } } defRangeDrawable = a.getDrawable(io.apptik.widget.mslider.R.styleable .MultiSlider_range); if (defRangeDrawable == null) { defRangeDrawable = ContextCompat.getDrawable(getContext(), R.drawable.multislider_range_material ); } Drawable range1Drawable = a.getDrawable(io.apptik.widget.mslider.R.styleable .MultiSlider_range1); Drawable range2Drawable = a.getDrawable(io.apptik.widget.mslider.R.styleable .MultiSlider_range2); defRangeColor = a.getColor(io.apptik.widget.mslider.R.styleable.MultiSlider_rangeColor, 0); defThumbColor = a.getColor(io.apptik.widget.mslider.R.styleable.MultiSlider_thumbColor, 0); setThumbDrawables(defThumbDrawable, defRangeDrawable, range1Drawable, range2Drawable); // // will // guess thumbOffset if // thumb != null... // ...but allow layout to override this int thumbOffset = a.getDimensionPixelOffset(io.apptik.widget.mslider.R.styleable .MultiSlider_android_thumbOffset, defThumbDrawable.getIntrinsicWidth() / 2); setThumbOffset(thumbOffset); repositionThumbs(); mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mNoInvalidate = false; a.recycle(); } /** * @return max number of steps thumb vales can differ */ public int getStepsThumbsApart() { return mStepsThumbsApart; } /** * @param stepsThumbsApart max number of steps thumb vales can differ */ public void setStepsThumbsApart(int stepsThumbsApart) { if (stepsThumbsApart < 0) stepsThumbsApart = 0; this.mStepsThumbsApart = stepsThumbsApart; } /** * @return Step value in scale points */ public int getStep() { return mStep; } /** * @param mStep Step value in scale points */ public void setStep(int mStep) { this.mStep = mStep; } /** * @return number of scale points */ public int getScaleSize() { return mScaleMax - mScaleMin; } /** * Re-position thumbs so they are equally distributed according to the scale */ public void repositionThumbs() { 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; } } } /** * Listener for value changes and start/stop of thumb move. * * @param l */ public void setOnThumbValueChangeListener(OnThumbValueChangeListener l) { mOnThumbValueChangeListener = l; } /** * Listener for value changes and start/stop of thumb move. * * @param l */ public void setOnTrackingChangeListener(OnTrackingChangeListener l) { mOnTrackingChangeListener = l; } /** * @return true if thumbs will be not be drawn on top of each other even in have the same * values, false otherwise */ public boolean isDrawThumbsApart() { return mDrawThumbsApart; } /** * @param drawThumbsApart if set to true thumbs will be not be drawn on top of each other * even in have the same values. */ 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).setTag("thumb " + i)); } } /** * Re-sets the number of thumbs and reposition the thumbs * @param numThumbs the new number of thumbs * @return the MultiSlider */ public MultiSlider setNumberOfThumbs(int numThumbs) { return setNumberOfThumbs(numThumbs, true); } /** * Re-sets the number of thumbs * @param numThumbs the new number of thumbs * @param repositon if true it will reposition the thumbs to be equally distributed across the * scale, otherwise all thumbs will be positioned at 0 * @return the MultiSlider */ public MultiSlider setNumberOfThumbs(int numThumbs, boolean repositon) { clearThumbs(); for (int i = 0; i < numThumbs; i++) { addThumb(0); } if(repositon) { repositionThumbs(); } return this; } /** * Add a thumb to the Slider after the last thumb * * @param thumb Thumb instance in the context of the Slider * @return true if the thumb was added and Slider modified */ public boolean addThumb(Thumb thumb) { return addThumbOnPos(thumb, mThumbs.size()); } /** * Add a thumb to the Slider at a custom position * @param thumb thumb instance in the context of the Slider * @param pos the position at which the thumb should be added * @return true if the thumb was added and Slider modified */ public boolean addThumbOnPos(Thumb thumb, int pos) { if (mThumbs.contains(thumb)) { return false; } if (thumb.getThumb() == null) { setThumbDrawable(thumb, defThumbDrawable, defThumbColor); } int paddingLeft = Math.max(getPaddingLeft(), thumb.getThumbOffset()); int paddingRight = Math.max(getPaddingRight(), thumb.getThumbOffset()); setPadding(paddingLeft, getPaddingTop(), paddingRight, getPaddingBottom()); if (thumb.getRange() == null) { setRangeDrawable(thumb, defRangeDrawable, defRangeColor); } mThumbs.add(pos, thumb); setThumbValue(thumb, thumb.value, false); return true; } /** * Add a thumb with predefined value to the slider after the last thumb * @param value the initial thumb value * @return the Thumb instance that was added */ public Thumb addThumb(int value) { Thumb thumb = new Thumb(); this.addThumb(thumb); thumb.setValue(value); return thumb; } /** * Add a thumb to the slider after the last thumb with value to the maximum scale value * @return the Thumb instance that was added */ public Thumb addThumb() { Thumb thumb = new Thumb(); this.addThumb(thumb); return thumb; } /** * Add a thumb to the Slider at a custom position * @param pos the position at which the thumb should be added * @param value the initial thumb value * @return the Thumb instance that was added */ public Thumb addThumbOnPos(int pos, int value) { Thumb thumb = new Thumb(); this.addThumbOnPos(thumb, pos); thumb.setValue(value); return thumb; } /** * Add a thumb to the Slider at a custom position * @param pos the position at which the thumb should be added * @return the Thumb instance that was added */ public Thumb addThumbOnPos(int pos) { Thumb thumb = new Thumb(); this.addThumbOnPos(thumb, pos); return thumb; } /** * Remove a thumb from the Slider * * @param thumb humb instance in the context of the Slider * @return true if the thumb was found and removed */ public boolean removeThumb(Thumb thumb) { mDraggingThumbs.remove(thumb); boolean res = mThumbs.remove(thumb); invalidate(); return res; } /** * Remove a thumb from the Slider identified by its position * * @param thumbIndex the thumb position starting from 0 * @return true if the thumb was found and removed */ public Thumb removeThumb(int thumbIndex) { mDraggingThumbs.remove(mThumbs.get(thumbIndex)); invalidate(); Thumb res = mThumbs.remove(thumbIndex); invalidate(); return res; } /** * Removes all the thumbs in the Slider */ public void clearThumbs() { mThumbs.clear(); mDraggingThumbs.clear(); invalidate(); } /** * set default thumb offset, which will be immediately applied to all the thumbs * * @param thumbOffset thumb offset in pixels */ public void setThumbOffset(int thumbOffset) { for (Thumb thumb : mThumbs) { thumb.setThumbOffset(thumbOffset); } invalidate(); } /** * Manually set the track drawable * * @param d */ 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; 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; int rCol; for (Thumb mThumb : mThumbs) { curr++; if (mThumb.getThumb() != null && thumb != mThumb.getThumb()) { mThumb.getThumb().setCallback(null); } if (curr == 1 && range1 != null) { rangeDrawable = range1; rCol = a.getColor(io.apptik.widget.mslider.R.styleable.MultiSlider_range1Color, 0); } else if (curr == 2 && range2 != null) { rangeDrawable = range2; rCol = a.getColor(io.apptik.widget.mslider.R.styleable.MultiSlider_range2Color, 0); } else { rangeDrawable = range; rCol = defRangeColor; } setRangeDrawable(mThumb, rangeDrawable, rCol); setThumbDrawable(mThumb, thumb, defThumbColor); padding = Math.max(padding, mThumb.getThumbOffset()); } setPadding(padding, getPaddingTop(), padding, getPaddingBottom()); } private void setThumbDrawable(Thumb thumb, Drawable thumbDrawable, int thumbColor) { requireNonNull(thumbDrawable); Drawable nThumbDrawable = getTintedDrawable(thumbDrawable.getConstantState().newDrawable(), thumbColor); nThumbDrawable.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. thumb.setThumbOffset(thumbDrawable.getIntrinsicWidth() / 2); // If we're updating get the new states if (thumb.getThumb() != null && (nThumbDrawable.getIntrinsicWidth() != thumb.getThumb() .getIntrinsicWidth() || nThumbDrawable.getIntrinsicHeight() != thumb.getThumb().getIntrinsicHeight())) { requestLayout(); } thumb.setThumb(nThumbDrawable); invalidate(); if (nThumbDrawable != null && nThumbDrawable.isStateful()) { // Note that if the states are different this won't work. // For now, let's consider that an app bug. int[] state = getDrawableState(); nThumbDrawable.setState(state); } } private void setRangeDrawable(Thumb thumb, Drawable rangeDrawable, int rangeColor) { requireNonNull(rangeDrawable); Drawable nRangeDrawable = getTintedDrawable(rangeDrawable, rangeColor); thumb.setRange(nRangeDrawable); } /** * Return the Thumb by its positions - the component that * the user can drag back and forth. * * @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; } /** * Set global maximum value and apply it to all thumbs * * @param max maximum value in scale points */ public synchronized void setMax(int max) { setMax(max, true, false); } /** * Set global maximum value * * @param max maximum value in scale points * @param extendMaxForThumbs if set to true the new max will be applied to all the thumbs. */ public synchronized void setMax(int max, boolean extendMaxForThumbs) { setMax(max, extendMaxForThumbs, false); } /** * Set global maximum value * * @param max maximum value in scale points * @param extendMaxForThumbs if set to true the new max will be applied to all the thumbs. * @param repositionThumbs if set to true the thumbs will change their value and be placed on * equal distances from each other respecting the new scale */ public synchronized void setMax(int max, boolean extendMaxForThumbs, boolean repositionThumbs) { if (max < mScaleMin) { throw new IllegalArgumentException(String.format("setMax(%d) < Min(%d)",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) repositionThumbs(); 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 int getMax() { return mScaleMax; } /** * Set global minimum value and apply it to all thumbs * * @param min minimum value in scale points */ public synchronized void setMin(int min) { setMin(min, true, false); } /** * Set global minimum value * * @param min minimum value in scale points * @param extendMinForThumbs if set to true the new min will be applied to all the thumbs. */ public synchronized void setMin(int min, boolean extendMinForThumbs) { setMin(min, extendMinForThumbs, false); } /** * Set global minimum value * * @param min minimum value in scale points * @param extendMinForThumbs if set to true the new min will be applied to all the thumbs. * @param repositionThumbs if set to true the thumbs will change their value and be placed on * equal distances from each other respecting the new scale */ public synchronized void setMin(int min, boolean extendMinForThumbs, boolean repositionThumbs) { if (min > mScaleMax) { throw new IllegalArgumentException(String.format("setMin(%d) > Max(%d)",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 (extendMinForThumbs) { thumb.setMin(min); } else if (thumb.getMin() < min) { thumb.setMin(min); } if (thumb.getValue() < min) { setThumbValue(thumb, min, false); } } if (repositionThumbs) repositionThumbs(); 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 int getMin() { return mScaleMin; } @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()) { if (thumb.isEnabled()) { thumb.getThumb().setState(new int[]{android.R.attr.state_enabled}); } else { thumb.getThumb().setState(new int[]{-android.R.attr.state_enabled}); } } } } else { for (Thumb thumb : mThumbs) { if (thumb.getThumb() != null && thumb.getThumb().isStateful()) { if (thumb.isEnabled()) { thumb.getThumb().setState(new int[]{android.R.attr.state_enabled}); } else { thumb.getThumb().setState(new int[]{-android.R.attr.state_enabled}); } } } } } /** * 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; } final int thumbStart = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos + optThumbOffset : thumbPos + optThumbOffset; thumb.setBounds(thumbStart, topBound, thumbStart + thumbWidth, bottomBound); int bottom = h - getPaddingTop() + getPaddingBottom(); int rangeStart = 0; if (isLayoutRtl() && mMirrorForRtl) { rangeStart = available; } if (prevThumb != null) { rangeStart = prevThumb.getBounds().left; } if (range != null) { if (isLayoutRtl() && mMirrorForRtl) { range.setBounds(thumbStart, 0, rangeStart + optThumbOffset, bottom); } else { range.setBounds(rangeStart, 0, thumbStart, bottom); } } invalidate(); } @Override protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); int paddingStart; if (Build.VERSION.SDK_INT >= 17) { paddingStart = getPaddingStart(); } else { paddingStart = getPaddingLeft(); } // --> draw track if (mTrack != null) { canvas.save(); canvas.translate(paddingStart, getPaddingTop()); mTrack.draw(canvas); canvas.restore(); } // --> draw ranges for (Thumb thumb : mThumbs) { if (thumb.getRange() != null) { canvas.save(); canvas.translate(paddingStart, 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(paddingStart - 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) { if (isLayoutRtl() && mMirrorForRtl) { available -= getThumbOptOffset(mThumbs.getFirst()); } else { 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.isEnabled() || mDraggingThumbs.contains(thumb)) 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) { if (thumb.getThumb() == null || !thumb.isEnabled() || mDraggingThumbs.contains(thumb)) continue; 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; } private Thumb getMostMovableThumb(MotionEvent event) { if (exactTouched == null || exactTouched.size() < 1) return null; if (exactTouched.size() == 1) { return exactTouched.getFirst(); } else { return getMostMovable(exactTouched, event); } } @Override public boolean onTouchEvent(MotionEvent event) { if (!mIsUserSeekable || !isEnabled()) { return false; } final int xx = Math.round(event.getX()); final int yy = Math.round(event.getY()); int pointerIdx = event.getActionIndex(); Thumb currThumb = null; if (event.getActionMasked() == MotionEvent.ACTION_DOWN || event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { LinkedList<Thumb> closestOnes = getClosestThumb((int) event.getX(pointerIdx)); if (isInScrollingContainer() && mDraggingThumbs.size() == 0 && exactTouched != null && pointerIdx > 0) { //we have been here before => we want to use the bar Thumb prevThumb = exactTouched.getFirst(); onStartTrackingTouch(prevThumb); exactTouched = null; } if (closestOnes != null && !closestOnes.isEmpty()) { if (closestOnes.size() == 1) { currThumb = closestOnes.getFirst(); if (isInScrollingContainer() && mDraggingThumbs.size() == 0) { exactTouched = closestOnes; } } else { //we have more than one thumb at the same place and we touched there exactTouched = closestOnes; } } } else if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { if (exactTouched != null && !exactTouched.isEmpty()) { currThumb = getMostMovableThumb(event); //check if move actually changed value // if (currThumb == null) return false; } else if (mDraggingThumbs.size() > pointerIdx) { currThumb = mDraggingThumbs.get(pointerIdx); } } else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) { if (mDraggingThumbs.size() > pointerIdx) { currThumb = mDraggingThumbs.get(pointerIdx); } //else we had a candidate but was never tracked else if (exactTouched != null && exactTouched.size() > 0) { currThumb = getMostMovableThumb(event); exactTouched = null; } } // else { // LinkedList<Thumb> closestOnes = getClosestThumb((int) event.getX()); // currThumb = closestOnes.getFirst(); // } switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: if (isInScrollingContainer() && mDraggingThumbs.size() == 0) { mTouchDownX = event.getX(pointerIdx); } else { onStartTrackingTouch(currThumb); setThumbValue(currThumb, getValue(event, currThumb), true); setHotspot(xx, yy, currThumb); } break; //with move we dont have pointer action so set them all case MotionEvent.ACTION_MOVE: if (mDraggingThumbs.contains(currThumb)) { //need the index for (int i = 0; i < mDraggingThumbs.size(); i++) { if (mDraggingThumbs.get(i) != null && mDraggingThumbs.get(i).getThumb() != null) { invalidate(mDraggingThumbs.get(i).getThumb().getBounds()); } setThumbValue(mDraggingThumbs.get(i), getValue(event, i, mDraggingThumbs .get(i)), true); } setHotspot(xx, yy, currThumb); } else { final float x = event.getX(pointerIdx); if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) { onStartTrackingTouch(currThumb); exactTouched = null; setThumbValue(currThumb, getValue(event, currThumb), true); setHotspot(xx, yy, currThumb); } } break; case MotionEvent.ACTION_UP: setPressed(false); //there are other pointers left case MotionEvent.ACTION_POINTER_UP: if (currThumb != null) { setThumbValue(currThumb, getValue(event, currThumb), true); setHotspot(xx, yy, currThumb); boolean toUnPress = false; if (!isPressed()) { setPressed(true); toUnPress = true; } onStopTrackingTouch(currThumb); if (toUnPress) { setPressed(false); } } 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; 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); } private void setHotspot(float x, float y, Thumb thumb) { if (thumb == null || thumb.getThumb() == null) return; final Drawable background = getBackground(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && background != null) { background.setHotspot(x, y); Rect rect = thumb.getThumb().getBounds(); final int offsetY = getPaddingTop(); background.setHotspotBounds(rect.left, rect.top + offsetY, rect.right, rect.bottom + offsetY); } } int getThumbOptOffset(Thumb thumb) { if (!mDrawThumbsApart) return 0; if (thumb == null || thumb.getThumb() == null) return 0; int thumbIdx = mThumbs.indexOf(thumb); if (isLayoutRtl() && mMirrorForRtl) { 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() && mMirrorForRtl) { 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) { setPressed(true); if (thumb.getThumb() != null) { // This may be within the padding region. invalidate(thumb.getThumb().getBounds()); } mDraggingThumbs.add(thumb); drawableStateChanged(); if (hasOnTrackingChangeListener()) { mOnTrackingChangeListener.onStartTrackingTouch(this, thumb, thumb.getValue()); } attemptClaimDrag(); } } /** * 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 (hasOnTrackingChangeListener()) { mOnTrackingChangeListener.onStopTrackingTouch(this, thumb, thumb.getValue()); } drawableStateChanged(); } } void onStopTrackingTouch() { mDraggingThumbs.clear(); } private boolean hasOnThumbValueChangeListener() { return mOnThumbValueChangeListener != null; } // private boolean hasOnTrackingChangeListener() { return mOnTrackingChangeListener != 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 AccessibilityNodeProvider getAccessibilityNodeProvider() { if (mAccessibilityNodeProvider == null) { mAccessibilityNodeProvider = new VirtualTreeProvider(); } return mAccessibilityNodeProvider; } // @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(MultiSlider.class.getName()); } @Override public void onRtlPropertiesChanged(int layoutDirection) { if (Build.VERSION.SDK_INT >= 17) { super.onRtlPropertiesChanged(layoutDirection); invalidate(); } // // 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; } /** * Void listener helper */ public static class SimpleChangeListener implements OnThumbValueChangeListener, OnTrackingChangeListener { @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) { } } class VirtualTreeProvider extends AccessibilityNodeProvider { static final int ACT_SET_PROGRESS = 16908349; final AccessibilityNodeInfo.AccessibilityAction ACTION_SET_PROGRESS; public VirtualTreeProvider() { if (Build.VERSION.SDK_INT >= 21) { ACTION_SET_PROGRESS = new AccessibilityNodeInfo.AccessibilityAction(ACT_SET_PROGRESS, null); } else { ACTION_SET_PROGRESS = null; } } @Override public AccessibilityNodeInfo createAccessibilityNodeInfo(int thumbId) { AccessibilityNodeInfo info = null; if (thumbId == View.NO_ID) { // We are requested to create an AccessibilityNodeInfo describing // this View, i.e. the root of the virtual sub-tree. Note that the // host View has an AccessibilityNodeProvider which means that this // provider is responsible for creating the node info for that root. info = AccessibilityNodeInfo.obtain(MultiSlider.this); onInitializeAccessibilityNodeInfo(info); // Add the virtual children of the root View. final int childCount = mThumbs.size(); for (int i = 0; i < childCount; i++) { info.addChild(MultiSlider.this, i); } if (mThumbs.size() == 1) { info.setScrollable(true); if (Build.VERSION.SDK_INT >= 21) { info.addAction(ACTION_SET_PROGRESS); info.addAction(ACTION_SCROLL_BACKWARD); info.addAction(ACTION_SCROLL_FORWARD); } else { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } } } else { // Find the view that corresponds to the given id. Thumb thumb = mThumbs.get(thumbId); if (thumb == null) { return null; } // Obtain and initialize an AccessibilityNodeInfo with // information about the virtual view. info = AccessibilityNodeInfo.obtain(MultiSlider.this, thumbId); info.setClassName(thumb.getClass().getName()); info.setParent(MultiSlider.this); info.setSource(MultiSlider.this, thumbId); info.setContentDescription("Multi-Slider thumb no:" + thumbId); if (Build.VERSION.SDK_INT >= 21) { info.addAction(ACTION_SET_PROGRESS); if (thumb.getPossibleMax() > thumb.value) { info.addAction(ACTION_SCROLL_BACKWARD); } if (thumb.getPossibleMax() > thumb.value) { info.addAction(ACTION_SCROLL_FORWARD); } } else { if (thumb.getPossibleMin() > thumb.value) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } if (thumb.getPossibleMax() > thumb.value) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } } if (thumb.getThumb() != null) { int[] loc = new int[2]; getLocationOnScreen(loc); Rect rect = thumb.getThumb().copyBounds(); rect.top += loc[1]; rect.left += loc[0]; rect.right += loc[0]; rect.bottom += loc[1]; info.setBoundsInScreen(rect); //TODO somehow this resuls in [0,0][0,0]. wonder check why //info.setBoundsInParent(rect); } info.setText(thumb.tag + ": " + thumb.value); info.setEnabled(thumb.isEnabled()); if (Build.VERSION.SDK_INT >= 24) { info.setImportantForAccessibility(true); } info.setVisibleToUser(true); info.setScrollable(true); } return info; } @Override public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText( String searched, int virtualViewId) { if (TextUtils.isEmpty(searched)) { return Collections.emptyList(); } String searchedLowerCase = searched.toLowerCase(); List<AccessibilityNodeInfo> result = null; if (virtualViewId == View.NO_ID) { // If the search is from the root, i.e. this View, go over the virtual // children and look for ones that contain the searched string since // this View does not contain text itself. final int childCount = mThumbs.size(); for (int i = 0; i < childCount; i++) { Thumb child = mThumbs.get(i); String textToLowerCase = child.tag.toLowerCase(); if (textToLowerCase.contains(searchedLowerCase)) { if (result == null) { result = new ArrayList<>(); } result.add(createAccessibilityNodeInfo(i)); } } } else { // If the search is from a virtual view, find the view. Since the tree // is one level deep we add a node info for the child to the result if // the child contains the searched text. Thumb virtualView = mThumbs.get(virtualViewId); if (virtualView != null) { String textToLowerCase = virtualView.tag.toLowerCase(); if (textToLowerCase.contains(searchedLowerCase)) { result = new ArrayList<>(); result.add(createAccessibilityNodeInfo(virtualViewId)); } } } if (result == null) { return Collections.emptyList(); } return result; } @Override public AccessibilityNodeInfo findFocus(int focus) { return super.findFocus(focus); } @Override public boolean performAction(int virtualViewId, int action, Bundle arguments) { if (virtualViewId == View.NO_ID) { //do nothing .. for now return false; } else { if (virtualViewId >= mThumbs.size()) return false; Thumb thumb = mThumbs.get(virtualViewId); if (thumb == null) return false; switch (action) { case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: thumb.setValue(thumb.value + getStep()); return true; case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: thumb.setValue(thumb.value - getStep()); return true; case ACT_SET_PROGRESS: thumb.setValue(arguments.getInt("value")); return true; } } return false; } } }