/* * Copyright (C) 2016 ceryle * * 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 co.ceryle.segmentedbutton; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.support.v4.view.animation.FastOutLinearInInterpolator; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.support.v4.view.animation.LinearOutSlowInInterpolator; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateInterpolator; import android.view.animation.AnticipateInterpolator; import android.view.animation.AnticipateOvershootInterpolator; import android.view.animation.BounceInterpolator; import android.view.animation.CycleInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.OvershootInterpolator; import android.widget.FrameLayout; import android.widget.LinearLayout; import java.util.ArrayList; public class SegmentedButtonGroup extends LinearLayout { public SegmentedButtonGroup(Context context) { super(context); init(null); } public SegmentedButtonGroup(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public SegmentedButtonGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public SegmentedButtonGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } private LinearLayout mainGroup, rippleContainer, dividerContainer; private boolean draggable = false; @Override public boolean onTouchEvent(MotionEvent event) { float selectorWidth, offsetX; int position = 0; switch (event.getAction()) { case MotionEvent.ACTION_UP: selectorWidth = (float) getWidth() / numberOfButtons / 2f; offsetX = ((event.getX() - selectorWidth) * numberOfButtons) / getWidth(); position = (int) Math.floor(offsetX + 0.5); toggledPositionOffset = lastPositionOffset = offsetX; toggle(position, animateSelectorDuration, true); break; case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: if (!draggable) break; selectorWidth = (float) getWidth() / numberOfButtons / 2f; offsetX = ((event.getX() - selectorWidth) * numberOfButtons) / (float) getWidth(); position = (int) Math.floor(offsetX); offsetX -= position; if (event.getRawX() - selectorWidth < getLeft()) { offsetX = 0; animateViews(position + 1, offsetX); break; } if (event.getRawX() + selectorWidth > getRight()) { offsetX = 1; animateViews(position - 1, offsetX); break; } animateViews(position, offsetX); break; } return true; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private class ButtonOutlineProvider extends ViewOutlineProvider { @Override public void getOutline(View view, Outline outline) { outline.setRoundRect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight(), radius); } } private void init(AttributeSet attrs) { getAttributes(attrs); setWillNotDraw(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setOutlineProvider(new ButtonOutlineProvider()); } setClickable(true); buttons = new ArrayList<>(); FrameLayout container = new FrameLayout(getContext()); container.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); addView(container); mainGroup = new LinearLayout(getContext()); mainGroup.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); mainGroup.setOrientation(LinearLayout.HORIZONTAL); container.addView(mainGroup); rippleContainer = new LinearLayout(getContext()); rippleContainer.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); rippleContainer.setOrientation(LinearLayout.HORIZONTAL); rippleContainer.setClickable(false); rippleContainer.setFocusable(false); rippleContainer.setPadding(borderSize, borderSize, borderSize, borderSize); container.addView(rippleContainer); dividerContainer = new LinearLayout(getContext()); dividerContainer.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); dividerContainer.setOrientation(LinearLayout.HORIZONTAL); dividerContainer.setClickable(false); dividerContainer.setFocusable(false); container.addView(dividerContainer); initInterpolations(); setContainerAttrs(); setDividerAttrs(); rectF = new RectF(); paint = new Paint(Paint.ANTI_ALIAS_FLAG); } private RectF rectF; private Paint paint; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float width = canvas.getWidth(); float height = canvas.getHeight(); rectF.set(0, 0, width, height); paint.setStyle(Paint.Style.FILL); paint.setColor(backgroundColor); canvas.drawRoundRect(rectF, radius, radius, paint); if (borderSize > 0) { float bSize = borderSize / 2f; rectF.set(0 + bSize, 0 + bSize, width - bSize, height - bSize); paint.setStyle(Paint.Style.STROKE); paint.setColor(borderColor); paint.setStrokeWidth(borderSize); canvas.drawRoundRect(rectF, radius, radius, paint); } } private void setBackgroundColor(View v, Drawable d, int c) { if (null != d) { BackgroundHelper.setBackground(v, d); } else { v.setBackgroundColor(c); } } private void setDividerAttrs() { if (!hasDivider) return; dividerContainer.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE); // Divider Views RoundHelper.makeDividerRound(dividerContainer, dividerColor, dividerRadius, dividerSize, dividerBackgroundDrawable); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { dividerContainer.setDividerPadding(dividerPadding); } } private int numberOfButtons = 0; @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (child instanceof SegmentedButton) { SegmentedButton button = (SegmentedButton) child; final int position = numberOfButtons++; button.setSelectorColor(selectorColor); button.setSelectorRadius(radius); button.setBorderSize(borderSize); if (position == 0) button.hasBorderLeft(true); if (position > 0) buttons.get(position - 1).hasBorderRight(false); button.hasBorderRight(true); mainGroup.addView(child, params); buttons.add(button); if (this.position == position) { button.clipToRight(1); lastPosition = toggledPosition = position; lastPositionOffset = toggledPositionOffset = (float) position; } // RIPPLE BackgroundView rippleView = new BackgroundView(getContext()); if (!draggable) { rippleView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (clickable && enabled) toggle(position, animateSelectorDuration, true); } }); } setRipple(rippleView, enabled && clickable); rippleContainer.addView(rippleView, new LinearLayout.LayoutParams(button.getButtonWidth(), ViewGroup.LayoutParams.MATCH_PARENT, button.getWeight())); ripples.add(rippleView); if (!hasDivider) return; BackgroundView dividerView = new BackgroundView(getContext()); dividerContainer.addView(dividerView, new LinearLayout.LayoutParams(button.getButtonWidth(), ViewGroup.LayoutParams.MATCH_PARENT, button.getWeight())); } else super.addView(child, index, params); } private ArrayList<BackgroundView> ripples = new ArrayList<>(); private void setRipple(View v, boolean isClickable) { if (isClickable) { if (hasRippleColor) RippleHelper.setRipple(v, rippleColor, radius); else if (ripple) RippleHelper.setSelectableItemBackground(getContext(), v); else { for (View button : buttons) { if (button instanceof SegmentedButton && ((SegmentedButton) button).hasRipple()) RippleHelper.setRipple(v, ((SegmentedButton) button).getRippleColor(), radius); } } } else { BackgroundHelper.setBackground(v, null); } } private void setContainerAttrs() { if (isInEditMode()) mainGroup.setBackgroundColor(backgroundColor); } private ArrayList<SegmentedButton> buttons; private int selectorColor, animateSelector, animateSelectorDuration, position, backgroundColor, dividerColor, radius, dividerSize, rippleColor, dividerPadding, dividerRadius, borderSize, borderColor; private boolean clickable, enabled, ripple, hasRippleColor, hasDivider; private Drawable backgroundDrawable, selectorBackgroundDrawable, dividerBackgroundDrawable; /** * Get attributes **/ private void getAttributes(AttributeSet attrs) { TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SegmentedButtonGroup); hasDivider = typedArray.hasValue(R.styleable.SegmentedButtonGroup_sbg_dividerSize); dividerSize = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_dividerSize, 0); dividerColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_dividerColor, Color.WHITE); dividerPadding = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_dividerPadding, 0); dividerRadius = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_dividerRadius, 0); selectorColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_selectorColor, Color.GRAY); animateSelector = typedArray.getInt(R.styleable.SegmentedButtonGroup_sbg_animateSelector, 0); animateSelectorDuration = typedArray.getInt(R.styleable.SegmentedButtonGroup_sbg_animateSelectorDuration, 500); radius = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_radius, 0); position = typedArray.getInt(R.styleable.SegmentedButtonGroup_sbg_position, 0); backgroundColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_backgroundColor, Color.TRANSPARENT); ripple = typedArray.getBoolean(R.styleable.SegmentedButtonGroup_sbg_ripple, false); hasRippleColor = typedArray.hasValue(R.styleable.SegmentedButtonGroup_sbg_rippleColor); rippleColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_rippleColor, Color.GRAY); borderSize = typedArray.getDimensionPixelSize(R.styleable.SegmentedButtonGroup_sbg_borderSize, 0); borderColor = typedArray.getColor(R.styleable.SegmentedButtonGroup_sbg_borderColor, Color.BLACK); backgroundDrawable = typedArray.getDrawable(R.styleable.SegmentedButtonGroup_sbg_backgroundDrawable); selectorBackgroundDrawable = typedArray.getDrawable(R.styleable.SegmentedButtonGroup_sbg_selectorBackgroundDrawable); dividerBackgroundDrawable = typedArray.getDrawable(R.styleable.SegmentedButtonGroup_sbg_dividerBackgroundDrawable); enabled = typedArray.getBoolean(R.styleable.SegmentedButtonGroup_sbg_enabled, true); draggable = typedArray.getBoolean(R.styleable.SegmentedButtonGroup_sbg_draggable, false); try { clickable = typedArray.getBoolean(R.styleable.SegmentedButtonGroup_android_clickable, true); } catch (Exception ex) { Log.d("SegmentedButtonGroup", ex.toString()); } typedArray.recycle(); } private Interpolator interpolatorSelector; private void initInterpolations() { ArrayList<Class> interpolatorList = new ArrayList<Class>() {{ add(FastOutSlowInInterpolator.class); add(BounceInterpolator.class); add(LinearInterpolator.class); add(DecelerateInterpolator.class); add(CycleInterpolator.class); add(AnticipateInterpolator.class); add(AccelerateDecelerateInterpolator.class); add(AccelerateInterpolator.class); add(AnticipateOvershootInterpolator.class); add(FastOutLinearInInterpolator.class); add(LinearOutSlowInInterpolator.class); add(OvershootInterpolator.class); }}; try { interpolatorSelector = (Interpolator) interpolatorList.get(animateSelector).newInstance(); } catch (Exception e) { e.printStackTrace(); } } public final static int FastOutSlowInInterpolator = 0; public final static int BounceInterpolator = 1; public final static int LinearInterpolator = 2; public final static int DecelerateInterpolator = 3; public final static int CycleInterpolator = 4; public final static int AnticipateInterpolator = 5; public final static int AccelerateDecelerateInterpolator = 6; public final static int AccelerateInterpolator = 7; public final static int AnticipateOvershootInterpolator = 8; public final static int FastOutLinearInInterpolator = 9; public final static int LinearOutSlowInInterpolator = 10; public final static int OvershootInterpolator = 11; private OnPositionChangedListener onPositionChangedListener; /** * @param onPositionChangedListener set your instance that you have created to listen any position change */ public void setOnPositionChangedListener(OnPositionChangedListener onPositionChangedListener) { this.onPositionChangedListener = onPositionChangedListener; } /** * Use this listener if you want to know any position change. * Listener is called when one of segmented button is clicked or setPosition is called. */ public interface OnPositionChangedListener { void onPositionChanged(int position); } private OnClickedButtonListener onClickedButtonListener; /** * @param onClickedButtonListener set your instance that you have created to listen clicked positions */ public void setOnClickedButtonListener(OnClickedButtonListener onClickedButtonListener) { this.onClickedButtonListener = onClickedButtonListener; } /** * Use this listener if you want to know which button is clicked. * Listener is called when one of segmented button is clicked */ public interface OnClickedButtonListener { void onClickedButton(int position); } /** * @param position is used to select one of segmented buttons */ public void setPosition(int position) { this.position = position; if (null == buttons) { lastPosition = toggledPosition = position; lastPositionOffset = toggledPositionOffset = (float) position; } else { toggle(position, animateSelectorDuration, false); } } /** * @param position is used to select one of segmented buttons * @param duration determines how long animation takes to finish */ public void setPosition(int position, int duration) { this.position = position; if (null == buttons) { lastPosition = toggledPosition = position; lastPositionOffset = toggledPositionOffset = (float) position; } else { toggle(position, duration, false); } } /** * @param position is used to select one of segmented buttons * @param withAnimation if true default animation will perform */ public void setPosition(int position, boolean withAnimation) { this.position = position; if (null == buttons) { lastPosition = toggledPosition = position; lastPositionOffset = toggledPositionOffset = (float) position; } else { if (withAnimation) toggle(position, animateSelectorDuration, false); else toggle(position, 1, false); } } /** * @param selectorColor sets color to selector * default: Color.GRAY */ public void setSelectorColor(int selectorColor) { this.selectorColor = selectorColor; } /** * @param backgroundColor sets background color of whole layout including buttons on top of it * default: Color.WHITE */ @Override public void setBackgroundColor(int backgroundColor) { this.backgroundColor = backgroundColor; } /** * @param ripple applies android's default ripple on layout */ public void setRipple(boolean ripple) { this.ripple = ripple; } /** * @param rippleColor sets ripple color and adds ripple when a button is hovered * default: Color.GRAY */ public void setRippleColor(int rippleColor) { this.rippleColor = rippleColor; } /** * @param hasRippleColor if true ripple will be shown. * if setRipple(boolean) is also set to false, there will be no ripple */ public void setRippleColor(boolean hasRippleColor) { this.hasRippleColor = hasRippleColor; } /** * @param radius determines how round layout's corners should be */ public void setRadius(int radius) { this.radius = radius; } /** * @param dividerPadding adjusts divider's top and bottom distance to its container */ public void setDividerPadding(int dividerPadding) { this.dividerPadding = dividerPadding; } /** * @param animateSelectorDuration sets how long selector animation should last */ public void setSelectorAnimationDuration(int animateSelectorDuration) { this.animateSelectorDuration = animateSelectorDuration; } /** * @param animateSelector is used to give an animation to selector with the given interpolator constant */ public void setSelectorAnimation(int animateSelector) { this.animateSelector = animateSelector; } /** * @param interpolatorSelector is used to give an animation to selector with the given one of android's interpolator. * Ex: {@link FastOutSlowInInterpolator}, {@link BounceInterpolator}, {@link LinearInterpolator} */ public void setInterpolatorSelector(Interpolator interpolatorSelector) { this.interpolatorSelector = interpolatorSelector; } /** * @param dividerColor changes divider's color with the given one * default: Color.WHITE */ public void setDividerColor(int dividerColor) { this.dividerColor = dividerColor; RoundHelper.makeDividerRound(dividerContainer, dividerColor, dividerRadius, dividerSize, dividerBackgroundDrawable); } /** * @param dividerSize sets thickness of divider * default: 0 */ public void setDividerSize(int dividerSize) { this.dividerSize = dividerSize; RoundHelper.makeDividerRound(dividerContainer, dividerColor, dividerRadius, dividerSize, dividerBackgroundDrawable); } /** * @param dividerRadius determines how round divider should be * default: 0 */ public void setDividerRadius(int dividerRadius) { this.dividerRadius = dividerRadius; RoundHelper.makeDividerRound(dividerContainer, dividerColor, dividerRadius, dividerSize, dividerBackgroundDrawable); } /** * @param hasDivider if true divider will be shown. */ public void setDivider(boolean hasDivider) { this.hasDivider = hasDivider; } /** * @param borderSize sets thickness of border * default: 0 */ public void setBorderSize(int borderSize) { this.borderSize = borderSize; } /** * @param borderColor sets border color to the given one * default: Color.BLACK */ public void setBorderColor(int borderColor) { this.borderColor = borderColor; } public int getDividerSize() { return dividerSize; } public int getRippleColor() { return rippleColor; } public int getSelectorColor() { return selectorColor; } public int getSelectorAnimation() { return animateSelector; } public int getSelectorAnimationDuration() { return animateSelectorDuration; } public int getPosition() { return position; } public int getBackgroundColor() { return backgroundColor; } public int getDividerColor() { return dividerColor; } public float getRadius() { return radius; } public int getDividerPadding() { return dividerPadding; } public float getDividerRadius() { return dividerRadius; } public boolean isHasDivider() { return hasDivider; } public boolean isHasRippleColor() { return hasRippleColor; } public boolean isRipple() { return ripple; } public Interpolator getInterpolatorSelector() { return interpolatorSelector; } private void setRippleState(boolean state) { for (View v : ripples) { setRipple(v, state); } } private void setEnabledAlpha(boolean enabled) { float alpha = 1f; if (!enabled) alpha = 0.5f; setAlpha(alpha); } /** * @param enabled set it to: * false, if you want buttons to be unclickable and add grayish looking which gives disabled look, * true, if you want buttons to be clickable and remove grayish looking */ @Override public void setEnabled(boolean enabled) { this.enabled = enabled; setRippleState(enabled); setEnabledAlpha(enabled); } /** * @param clickable set it to: * false for unclickable buttons, * true for clickable buttons */ @Override public void setClickable(boolean clickable) { this.clickable = clickable; setRippleState(clickable); } @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable("state", super.onSaveInstanceState()); bundle.putInt("position", position); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; position = bundle.getInt("position"); state = bundle.getParcelable("state"); setPosition(position, false); } super.onRestoreInstanceState(state); } /** * * * * * * * * * */ private int toggledPosition = 0; private float toggledPositionOffset = 0; private void toggle(int position, int duration, boolean isToggledByTouch) { if (!draggable && toggledPosition == position) return; toggledPosition = position; ValueAnimator animator = ValueAnimator.ofFloat(toggledPositionOffset, position); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float animatedValue = toggledPositionOffset = (float) animation.getAnimatedValue(); int position = (int) animatedValue; float positionOffset = animatedValue - position; animateViews(position, positionOffset); invalidate(); } }); animator.setInterpolator(interpolatorSelector); animator.setDuration(duration); animator.start(); if (null != onClickedButtonListener && isToggledByTouch) onClickedButtonListener.onClickedButton(position); if (null != onPositionChangedListener) onPositionChangedListener.onPositionChanged(position); this.position = position; } private int lastPosition = 0; private float lastPositionOffset = 0; private void animateViews(int position, float positionOffset) { float realPosition = position + positionOffset; float lastRealPosition = lastPosition + lastPositionOffset; if (realPosition == lastRealPosition) { return; } int nextPosition = position + 1; if (positionOffset == 0.0f) { if (lastRealPosition <= realPosition) { nextPosition = position - 1; } } if (lastPosition > position) { if (lastPositionOffset > 0f) { toNextPosition(nextPosition + 1, 1); } } if (lastPosition < position) { if (lastPositionOffset < 1.0f) { toPosition(position - 1, 0); } } toNextPosition(nextPosition, 1.0f - positionOffset); toPosition(position, 1.0f - positionOffset); lastPosition = position; lastPositionOffset = positionOffset; } private void toPosition(int position, float clip) { if (position >= 0 && position < numberOfButtons) buttons.get(position).clipToRight(clip); } private void toNextPosition(int position, float clip) { if (position >= 0 && position < numberOfButtons) buttons.get(position).clipToLeft(clip); } }