package com.datdo.mobilib.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.Build;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.Scroller;
import android.widget.TextView;
import com.datdo.mobilib.R;
import com.datdo.mobilib.util.MblUtils;
/**
* <pre>
* iOS7のようなスイッチビュー。
* </pre>
*/
@SuppressLint("ClickableViewAccessibility")
public class MblSwitch extends FrameLayout {
private static final int VERTICAL_PADDING_IN_DP = 2;
private static final int NORMAL = 0;
private static final int BOLD = 1;
private static final int ITALIC = 2;
private static final int BOLD_ITALIC = 3;
private static final String DEFAULT_ON_TEXT = "On";
private static final String DEFAULT_OFF_TEXT = "Off";
private static final boolean DEFAULT_IS_ON = false;
private static final int DEFAULT_ON_COLOR = 0xff64bd63;
private static final int DEFAULT_OFF_COLOR = 0xff888888;
private static final int DEFAULT_TEXT_SIZE = MblUtils.pxFromSp(10);
private static final int DEFAULT_TEXT_COLOR = 0xffffffff;
private static final int DEFAULT_TEXT_STYLE = NORMAL;
private static final int DIRECTION_LEFT_TO_RIGHT = -1;
private static final int DIRECTION_RIGHT_TO_LEFT = 1;
private boolean mIsOn = DEFAULT_IS_ON;
private String mOnText;
private String mOffText;
private int mOnColor;
private int mOffColor;
private float mTextSize;
private int mTextColor;
private int mTextStyle;
private Scroller mScroller;
private GestureDetector mGestureDetector;
private float mCurrentX;
private boolean mFlingDetected;
private int mFlingDirection;
private boolean mDragging;
private boolean mSingleTapDetected;
private FrameLayout mLayoutBehind;
private View mKnobView;
private TextView mOnTextView;
private TextView mOffTextView;
private LinearLayout mLayoutOfTexts;
private MblSwitchCallback mCallback;
private boolean mInitialized;
public MblSwitch(Context context) {
super(context);
initViews(context);
}
public MblSwitch(Context context, AttributeSet attrs) {
super(context, attrs);
initAttr(context, attrs);
initViews(context);
}
public MblSwitch(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initAttr(context, attrs);
initViews(context);
}
private void initAttr(Context context, AttributeSet attrs) {
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MblSwitch, 0, 0);
mOnText = ta.getString(R.styleable.MblSwitch_onText);
if (mOnText == null) {
mOnText = DEFAULT_ON_TEXT;
}
mOffText = ta.getString(R.styleable.MblSwitch_offText);
if (mOffText == null) {
mOffText = DEFAULT_OFF_TEXT;
}
mOnColor = ta.getColor(R.styleable.MblSwitch_onColor, -1);
if (mOnColor == -1) {
mOnColor = DEFAULT_ON_COLOR;
}
mOffColor = ta.getColor(R.styleable.MblSwitch_offColor, -1);
if (mOffColor == -1) {
mOffColor = DEFAULT_OFF_COLOR;
}
mTextSize = ta.getDimension(R.styleable.MblSwitch_textSize, -1);
if (mTextSize == -1) {
mTextSize = DEFAULT_TEXT_SIZE;
}
mTextColor = ta.getColor(R.styleable.MblSwitch_textColor, -1);
if (mTextColor == -1) {
mTextColor = DEFAULT_TEXT_COLOR;
}
mTextStyle = ta.getInt(R.styleable.MblSwitch_textStyle, -1);
if (mTextStyle == -1) {
mTextStyle = DEFAULT_TEXT_STYLE;
}
mIsOn = ta.getBoolean(R.styleable.MblSwitch_isOn, DEFAULT_IS_ON);
}
@SuppressLint("NewApi")
private void initViews(final Context context) {
getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
MblUtils.removeOnGlobalLayoutListener(MblSwitch.this, this);
// create layout behind
final int verticalPadding = MblUtils.pxFromDp(VERTICAL_PADDING_IN_DP);
final int horizontalPadding = MblUtils.pxFromDp(2);
final int widthOfLayoutBehind = getWidth();
final int heightOfLayoutBehind = getHeight() - 2 * verticalPadding;
final int radius = heightOfLayoutBehind/2;
final RectF leftArcRect = new RectF(
horizontalPadding,
0,
2 * radius + horizontalPadding,
heightOfLayoutBehind);
final RectF rightArcRect = new RectF(
widthOfLayoutBehind - 2 * radius - horizontalPadding,
0,
widthOfLayoutBehind - horizontalPadding,
heightOfLayoutBehind);
mLayoutBehind = new FrameLayout(context) {
private Path mClipPath;
private Path getClipPath() {
if (mClipPath == null) {
mClipPath = new Path();
mClipPath.moveTo(radius + horizontalPadding, heightOfLayoutBehind);
mClipPath.arcTo(leftArcRect, 90, 180);
mClipPath.lineTo(widthOfLayoutBehind - radius - horizontalPadding, 0);
mClipPath.arcTo(rightArcRect, -90, 180);
mClipPath.close();
}
return mClipPath;
}
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.clipPath(getClipPath());
super.dispatchDraw(canvas);
}
};
if (Build.VERSION.SDK_INT >= 11) {
mLayoutBehind.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
FrameLayout.LayoutParams lpOfLayoutBehind = new FrameLayout.LayoutParams(widthOfLayoutBehind, heightOfLayoutBehind);
lpOfLayoutBehind.topMargin = verticalPadding;
lpOfLayoutBehind.gravity = Gravity.TOP | Gravity.LEFT;
mLayoutBehind.setLayoutParams(lpOfLayoutBehind);
mLayoutBehind.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false; // do not handle touch events
}
});
addView(mLayoutBehind);
// create layout to anti alias
View layoutAntiAlias = new View(context) {
private static final int DEGREE_PADDING = 5;
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw arc for ON
Paint onColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
onColorPaint.setAntiAlias(true);
onColorPaint.setColor(mOnColor);
onColorPaint.setStyle(Style.STROKE);
onColorPaint.setStrokeWidth(1);
canvas.drawArc(leftArcRect, 90 + DEGREE_PADDING, 180 - DEGREE_PADDING, true, onColorPaint);
// draw arc for OFF
Paint offColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
offColorPaint.setAntiAlias(true);
offColorPaint.setColor(mOffColor);
offColorPaint.setStyle(Style.STROKE);
offColorPaint.setStrokeWidth(1);
canvas.drawArc(rightArcRect, -90 + DEGREE_PADDING, 180 - DEGREE_PADDING, true, offColorPaint);
}
};
FrameLayout.LayoutParams lpOfLayoutAntiAlias = new FrameLayout.LayoutParams(widthOfLayoutBehind, heightOfLayoutBehind);
lpOfLayoutAntiAlias.topMargin = verticalPadding;
lpOfLayoutAntiAlias.gravity = Gravity.TOP | Gravity.LEFT;
layoutAntiAlias.setLayoutParams(lpOfLayoutAntiAlias);
addView(layoutAntiAlias, 0);
// create knob view
final int widthOfKnobView = getHeight();
final int heightOfKnobView = getHeight();
mKnobView = new View(context) {
private static final int COLOR = 0xffd3d3d3;
private static final int COLOR_PRESSED = 0xffe4e4e4;
private Paint mBgPaint;
private Paint getBgPaint() {
if (mBgPaint == null) {
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBgPaint.setStyle(Style.FILL);
mBgPaint.setAntiAlias(true);
}
mBgPaint.setColor(mDragging ? COLOR_PRESSED : COLOR);
return mBgPaint;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(
widthOfKnobView/2,
heightOfKnobView/2,
Math.min(widthOfKnobView, heightOfKnobView)/2,
getBgPaint());
}
};
FrameLayout.LayoutParams lpOfKnobView = new FrameLayout.LayoutParams(widthOfKnobView, heightOfKnobView);
lpOfKnobView.gravity = Gravity.LEFT | Gravity.TOP;
mKnobView.setLayoutParams(lpOfKnobView);
mKnobView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false; // do not handle touch events
}
});
addView(mKnobView);
// add text
final int widthOfTextView = widthOfLayoutBehind - widthOfKnobView + widthOfKnobView/2;
final int heighOfTextView = heightOfLayoutBehind;
int majorTextPadding = widthOfKnobView/2 + MblUtils.pxFromDp(3);
int minorTextPadding = MblUtils.pxFromDp(3) + horizontalPadding + heightOfLayoutBehind/4;
mOnTextView = generateTextView(context, mOnText, widthOfTextView, heighOfTextView, mOnColor,
minorTextPadding, majorTextPadding);
mOffTextView = generateTextView(context, mOffText, widthOfTextView, heighOfTextView, mOffColor,
majorTextPadding, minorTextPadding);
mLayoutOfTexts = new LinearLayout(context);
FrameLayout.LayoutParams lpOfLayoutOfTexts = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
lpOfLayoutOfTexts.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutOfTexts.setLayoutParams(lpOfLayoutOfTexts);
mLayoutOfTexts.setOrientation(LinearLayout.HORIZONTAL);
mLayoutOfTexts.addView(mOnTextView);
mLayoutOfTexts.addView(mOffTextView);
mLayoutBehind.addView(mLayoutOfTexts);
// scroller and gesture recognizer
mScroller = new Scroller(context);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
mDragging = true;
mFlingDetected = false;
mSingleTapDetected = false;
mKnobView.invalidate(); // redraw knob view 's background
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (mDragging) {
float toX = mCurrentX - distanceX;
toX = Math.max(toX, getMostLeftX());
toX = Math.min(toX, getMostRightX());
scrollTo(toX);
}
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
mSingleTapDetected = true;
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mFlingDetected = true;
mFlingDirection = velocityX > 0 ? DIRECTION_LEFT_TO_RIGHT : DIRECTION_RIGHT_TO_LEFT;
mScroller.fling(
(int) mCurrentX, 0,
(int) -velocityX, 0,
getMostLeftX(), getMostRightX(),
0, 0);
updateTranslationX();
return true;
}
});
// initialization is done
mInitialized = true;
// set initial status
MblUtils.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
setOn(mIsOn);
}
});
}
});
}
private void scrollTo(float toX) {
mScroller.startScroll((int) mCurrentX, 0, (int)(toX - mCurrentX), 0);
mCurrentX = toX;
updateTranslationX();
}
private void updateTranslationX() {
if (mScroller.computeScrollOffset()) {
setLeftMargin(mKnobView, mScroller.getCurrX());
setLeftMargin(mLayoutOfTexts, getCurrentXOfLayoutOfTexts(mScroller.getCurrX()));
}
if (!mScroller.isFinished()) {
MblUtils.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
updateTranslationX();
}
});
} else {
mCurrentX = mScroller.getCurrX();
}
}
private TextView generateTextView(
Context context,
String text,
int width,
int height,
int backgroundColor,
int paddingLeft,
int paddingRight) {
TextView textView = new TextView(context);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(width, height);
textView.setLayoutParams(lp);
textView.setGravity(Gravity.CENTER);
textView.setSingleLine(true);
textView.setEllipsize(TruncateAt.END);
textView.setPadding(paddingLeft, 0, paddingRight, 0);
textView.setBackgroundColor(backgroundColor);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
textView.setTextColor(mTextColor);
if (mTextStyle == NORMAL) {
textView.setTypeface(null, Typeface.NORMAL);
} else if (mTextStyle == BOLD) {
textView.setTypeface(null, Typeface.BOLD);
} else if (mTextStyle == ITALIC) {
textView.setTypeface(null, Typeface.ITALIC);
} else if (mTextStyle == BOLD_ITALIC) {
textView.setTypeface(null, Typeface.BOLD_ITALIC);
}
textView.setText(text);
return textView;
}
public void setOn(boolean isOn) {
if (mInitialized) {
mCurrentX = isOn ? getMostRightX() : getMostLeftX();
setLeftMargin(mLayoutOfTexts, getCurrentXOfLayoutOfTexts(mCurrentX));
setLeftMargin(mKnobView, (int) mCurrentX);
}
mIsOn = isOn;
}
public boolean isOn() {
return mIsOn;
}
private int getCurrentXOfLayoutOfTexts(float currentX) {
return (int) (currentX - mOnTextView.getWidth() + mKnobView.getWidth()/2);
}
private void setLeftMargin(View view, int leftMargin) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams();
lp.leftMargin = leftMargin;
view.setLayoutParams(lp);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
if (mFlingDetected) {
handleFling();
} else if (mSingleTapDetected) {
handleSingleTap();
} else {
handleFinishDragging();
}
mFlingDetected = false;
mDragging = false;
mSingleTapDetected = false;
mKnobView.invalidate(); // redraw knob view 's background
}
return true;
}
private void handleFinishDragging() {
float relativeCurrentX = mCurrentX + mKnobView.getWidth()/2;
boolean newOnStatus;
if (relativeCurrentX > getWidth() / 2) {
scrollTo(getMostRightX());
newOnStatus = true;
} else {
scrollTo(getMostLeftX());
newOnStatus = false;
}
boolean isChanged = mIsOn != newOnStatus;
mIsOn = newOnStatus;
if (mCallback != null && isChanged) {
mCallback.onStatusChanged(newOnStatus);
}
}
private void handleFling() {
boolean newOnStatus = mIsOn;
if (mFlingDirection == DIRECTION_LEFT_TO_RIGHT) {
scrollTo(getMostRightX());
newOnStatus = true;
} else if (mFlingDirection == DIRECTION_RIGHT_TO_LEFT) {
scrollTo(getMostLeftX());
newOnStatus = false;
}
boolean isChanged = mIsOn != newOnStatus;
mIsOn = newOnStatus;
if (mCallback != null && isChanged) {
mCallback.onStatusChanged(newOnStatus);
}
}
private void handleSingleTap() {
boolean newOnStatus;
if (mIsOn) {
scrollTo(getMostLeftX());
newOnStatus = false;
} else {
scrollTo(getMostRightX());
newOnStatus = true;
}
boolean isChanged = mIsOn != newOnStatus;
mIsOn = newOnStatus;
if (mCallback != null && isChanged) {
mCallback.onStatusChanged(newOnStatus);
}
}
private int getMostLeftX() {
return 0;
}
private int getMostRightX() {
return getWidth() - mKnobView.getWidth();
}
public static interface MblSwitchCallback {
public void onStatusChanged(boolean newOnStatus);
}
public void setCallback(MblSwitchCallback callback) {
mCallback = callback;
}
}