| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- package com.hjq.dialog.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.Rect;
- import android.graphics.Typeface;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.util.TypedValue;
- import android.view.GestureDetector;
- import android.view.MotionEvent;
- import android.view.View;
- import com.hjq.dialog.R;
- import java.util.List;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.ScheduledFuture;
- import java.util.concurrent.TimeUnit;
- /**
- * desc : 循环滚动列表自定义控件
- */
- public final class LoopView extends View {
- private static final String TAG = "LoopView";
- private ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor();
- private ScheduledFuture mScheduledFuture;
- private LoopScrollListener mListener;
- private List<String> mData;
- private Paint mTopBottomTextPaint;
- private Paint mCenterTextPaint;
- private Paint mCenterLinePaint;
- private int mTotalScrollY;
- private GestureDetector mGestureDetector;
- private int mSelectedItem;
- private int mTextSize;
- private int mMaxTextWidth;
- private int mMaxTextHeight;
- private int mTopBottomTextColor;
- private int mCenterTextColor;
- private int mCenterLineColor;
- private float mLineSpacingMultiplier;
- private boolean mCanLoop;
- private float mTopLineY;
- private float mBottomLineY;
- private int mCurrentIndex;
- private int mInitPosition;
- private float mHorizontalPadding;
- private float mVerticalPadding;
- private float mItemHeight;
- private int mDrawItemsCount;
- private String[] mItemTempArray;
- private float mCircularDiameter;
- private float mCircularRadius;
- public LoopView(Context context) {
- this(context, null);
- }
- public LoopView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
- public LoopView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoopView);
- if (array != null) {
- mTopBottomTextColor = array.getColor(R.styleable.LoopView_lv_topBottomTextColor, 0xffafafaf);
- mCenterTextColor = array.getColor(R.styleable.LoopView_lv_centerTextColor, 0xff313131);
- mCenterLineColor = array.getColor(R.styleable.LoopView_lv_lineColor, 0xffc5c5c5);
- mCanLoop = array.getBoolean(R.styleable.LoopView_lv_canLoop, true);
- mInitPosition = array.getInt(R.styleable.LoopView_lv_initPosition, -1);
- mTextSize = array.getDimensionPixelSize(R.styleable.LoopView_lv_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18, getContext().getResources().getDisplayMetrics()));
- mDrawItemsCount = array.getInt(R.styleable.LoopView_lv_drawItemCount, 7);
- mItemTempArray = new String[mDrawItemsCount];
- array.recycle();
- }
- mLineSpacingMultiplier = 3;
- mTopBottomTextPaint = new Paint();
- mCenterTextPaint = new Paint();
- mCenterLinePaint = new Paint();
- setLayerType(LAYER_TYPE_SOFTWARE, null);
- mGestureDetector = new GestureDetector(context, new LoopViewGestureListener());
- mGestureDetector.setIsLongpressEnabled(false);
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- switch (MeasureSpec.getMode(widthMeasureSpec)) {
- case MeasureSpec.AT_MOST:
- case MeasureSpec.UNSPECIFIED:
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxTextWidth, MeasureSpec.EXACTLY);
- break;
- case MeasureSpec.EXACTLY:
- break;
- }
- switch (MeasureSpec.getMode(heightMeasureSpec)) {
- case MeasureSpec.AT_MOST:
- case MeasureSpec.UNSPECIFIED:
- heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) mCircularDiameter, MeasureSpec.EXACTLY);
- break;
- case MeasureSpec.EXACTLY:
- break;
- }
- setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
- mItemHeight = mLineSpacingMultiplier * mMaxTextHeight;
- // auto calculate the text's left/right value when draw
- mHorizontalPadding = (width - mMaxTextWidth) / 2;
- mVerticalPadding = (height - mCircularDiameter) / 2;
- // topLineY = diameter/2 - itemHeight(mItemHeight) / 2 + mVerticalPadding
- mTopLineY = ((mCircularDiameter - mItemHeight) / 2) + mVerticalPadding;
- mBottomLineY = ((mCircularDiameter + mItemHeight) / 2) + mVerticalPadding;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- if (mData == null) return;
- // the length of single item is mItemHeight
- int mChangingItem = (int) (mTotalScrollY / (mItemHeight));
- mCurrentIndex = mInitPosition + mChangingItem % mData.size();
- if (!mCanLoop) { // can loop
- if (mCurrentIndex < 0) {
- mCurrentIndex = 0;
- }
- if (mCurrentIndex > mData.size() - 1) {
- mCurrentIndex = mData.size() - 1;
- }
- } else { // can not loop
- if (mCurrentIndex < 0) {
- mCurrentIndex = mData.size() + mCurrentIndex;
- }
- if (mCurrentIndex > mData.size() - 1) {
- mCurrentIndex = mCurrentIndex - mData.size();
- }
- }
- int count = 0;
- // reconfirm each item's value from dataList according to currentIndex,
- while (count < mDrawItemsCount) {
- int templateItem = mCurrentIndex - (mDrawItemsCount / 2 - count);
- if (mCanLoop) {
- if (templateItem < 0) {
- templateItem = templateItem + mData.size();
- }
- if (templateItem > mData.size() - 1) {
- templateItem = templateItem - mData.size();
- }
- mItemTempArray[count] = mData.get(templateItem);
- } else if (templateItem < 0) {
- mItemTempArray[count] = "";
- } else if (templateItem > mData.size() - 1) {
- mItemTempArray[count] = "";
- } else {
- mItemTempArray[count] = mData.get(templateItem);
- }
- count++;
- }
- // draw top and bottom line
- canvas.drawLine(0, mTopLineY, getMeasuredWidth(), mTopLineY, mCenterLinePaint);
- canvas.drawLine(0, mBottomLineY, getMeasuredWidth(), mBottomLineY, mCenterLinePaint);
- count = 0;
- int changingLeftY = (int) (mTotalScrollY % (mItemHeight));
- while (count < mDrawItemsCount) {
- canvas.save();
- // L= å * r -> å = rad
- float itemHeight = mMaxTextHeight * mLineSpacingMultiplier;
- // get radian L = (itemHeight * count - changingLeftY),r = mCircularRadius
- double radian = (itemHeight * count - changingLeftY) / mCircularRadius;
- // a = rad * 180 / π
- // get angle
- float angle = (float) (radian * 180 / Math.PI);
- // when angle >= 180 || angle <= 0 don't draw
- if (angle >= 180F || angle <= 0F) {
- canvas.restore();
- } else {
- // translateY = r - r*cos(å) -
- // (Math.sin(radian) * mMaxTextHeight) / 2 this is text offset
- float translateY = (float) (mCircularRadius - Math.cos(radian) * mCircularRadius - (Math.sin(radian) * mMaxTextHeight) / 2) + mVerticalPadding;
- canvas.translate(0.0F, translateY);
- // scale offset = Math.sin(radian) -> 0 - 1
- canvas.scale(1.0F, (float) Math.sin(radian));
- if (translateY <= mTopLineY) {
- // draw text y between 0 -> mTopLineY,include incomplete text
- canvas.save();
- canvas.clipRect(0, 0, getMeasuredWidth(), mTopLineY - translateY);
- canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mTopBottomTextPaint);
- canvas.restore();
- canvas.save();
- canvas.clipRect(0, mTopLineY - translateY, getMeasuredWidth(), (int) (itemHeight));
- canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mCenterTextPaint);
- canvas.restore();
- } else if (mMaxTextHeight + translateY >= mBottomLineY) {
- // draw text y between mTopLineY -> mBottomLineY ,include incomplete text
- canvas.save();
- canvas.clipRect(0, 0, getMeasuredWidth(), mBottomLineY - translateY);
- canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mCenterTextPaint);
- canvas.restore();
- canvas.save();
- canvas.clipRect(0, mBottomLineY - translateY, getMeasuredWidth(), (int) (itemHeight));
- canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mTopBottomTextPaint);
- canvas.restore();
- } else if (translateY >= mTopLineY && mMaxTextHeight + translateY <= mBottomLineY) {
- // draw center complete text
- canvas.clipRect(0, 0, getMeasuredWidth(), (int) (itemHeight));
- canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mCenterTextPaint);
- // center one indicate selected item
- mSelectedItem = mData.indexOf(mItemTempArray[count]);
- }
- canvas.restore();
- }
- count++;
- }
- }
- @SuppressLint("ClickableViewAccessibility")
- @Override
- public boolean onTouchEvent(MotionEvent motionevent) {
- switch (motionevent.getAction()) {
- case MotionEvent.ACTION_UP:
- default:
- if (!mGestureDetector.onTouchEvent(motionevent)) {
- startSmoothScrollTo();
- }
- }
- return true;
- }
- public final void setCanLoop(boolean canLoop) {
- mCanLoop = canLoop;
- invalidate();
- }
- /**
- * set text size
- *
- * @param size size indicate sp,not px
- */
- public final void setTextSize(float size) {
- if (size > 0) {
- mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, getContext().getResources().getDisplayMetrics());
- }
- }
- public void setLineSpacingMultiplier(float spacing) {
- this.mLineSpacingMultiplier = spacing;
- }
- public int getSelectedItem() {
- return mSelectedItem;
- }
- public void setInitPosition(int initPosition) {
- if (mData == null) return;
- if (initPosition > mData.size()) {
- initPosition = mData.size() - 1;
- }
- mInitPosition = initPosition;
- invalidate();
- if (mListener != null) {
- mListener.onItemSelect(this, initPosition);
- }
- }
- public void setLoopListener(LoopScrollListener l) {
- mListener = l;
- }
- /**
- * All public method must be called before this method
- * @param data data list
- */
- public final void setData(List<String> data) {
- mData = data;
- if (mData == null) {
- throw new IllegalArgumentException("data list must not be null!");
- }
- mTopBottomTextPaint.setColor(mTopBottomTextColor);
- mTopBottomTextPaint.setAntiAlias(true);
- mTopBottomTextPaint.setTypeface(Typeface.MONOSPACE);
- mTopBottomTextPaint.setTextSize(mTextSize);
- mCenterTextPaint.setColor(mCenterTextColor);
- mCenterTextPaint.setAntiAlias(true);
- mCenterTextPaint.setTextScaleX(1.05F);
- mCenterTextPaint.setTypeface(Typeface.MONOSPACE);
- mCenterTextPaint.setTextSize(mTextSize);
- mCenterLinePaint.setColor(mCenterLineColor);
- mCenterLinePaint.setAntiAlias(true);
- mCenterLinePaint.setTypeface(Typeface.MONOSPACE);
- mCenterLinePaint.setTextSize(mTextSize);
- // measureTextWidthHeight
- Rect rect = new Rect();
- for (int i = 0; i < mData.size(); i++) {
- String text = mData.get(i);
- mCenterTextPaint.getTextBounds(text, 0, text.length(), rect);
- int textWidth = rect.width();
- if (textWidth > mMaxTextWidth) {
- mMaxTextWidth = textWidth;
- }
- //int textHeight = rect.height();
- //if (textHeight > mMaxTextHeight) {
- // mMaxTextHeight = textHeight;
- //}
- Paint.FontMetrics fontMetrics = mCenterTextPaint.getFontMetrics();
- mMaxTextHeight = (int) (fontMetrics.bottom - fontMetrics.top) * 2 / 3;
- }
- // 计算半圆周 -- mMaxTextHeight * mLineSpacingMultiplier 表示每个item的高度 mDrawItemsCount = 7
- // 实际显示5个,留两个是在圆周的上下面
- // lineSpacingMultiplier是指text上下的距离的值和maxTextHeight一样的意思 所以 = 2
- // mDrawItemsCount - 1 代表圆周的上下两面各被剪切了一半 相当于高度少了一个 mMaxTextHeight
- int halfCircumference = (int) (mMaxTextHeight * mLineSpacingMultiplier * (mDrawItemsCount - 1));
- // the diameter of circular 2πr = cir, 2r = height
- mCircularDiameter = (int) ((halfCircumference * 2) / Math.PI);
- // the radius of circular
- mCircularRadius = (int) (halfCircumference / Math.PI);
- // FIXME: 7/8/16 通过控件的高度来计算圆弧的周长
- if (mInitPosition == -1) {
- if (mCanLoop) {
- mInitPosition = (mData.size() + 1) / 2;
- } else {
- mInitPosition = 0;
- }
- }
- mCurrentIndex = mInitPosition;
- invalidate();
- }
- private void cancelSchedule() {
- if (mScheduledFuture != null && !mScheduledFuture.isCancelled()) {
- mScheduledFuture.cancel(true);
- mScheduledFuture = null;
- }
- }
- private void startSmoothScrollTo() {
- int offset = (int) (mTotalScrollY % (mItemHeight));
- cancelSchedule();
- mScheduledFuture = mExecutor.scheduleWithFixedDelay(new HalfHeightRunnable(offset), 0, 10, TimeUnit.MILLISECONDS);
- }
- private void startSmoothScrollTo(float velocityY) {
- cancelSchedule();
- int velocityFling = 20;
- mScheduledFuture = mExecutor.scheduleWithFixedDelay(new FlingRunnable(velocityY), 0, velocityFling, TimeUnit.MILLISECONDS);
- }
- class LoopViewGestureListener extends android.view.GestureDetector.SimpleOnGestureListener {
- @Override
- public final boolean onDown(MotionEvent motionevent) {
- cancelSchedule();
- Log.i(TAG, "LoopViewGestureListener->onDown");
- return true;
- }
- @Override
- public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- startSmoothScrollTo(velocityY);
- Log.i(TAG, "LoopViewGestureListener->onFling");
- return true;
- }
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- Log.i(TAG, "LoopViewGestureListener->onScroll");
- mTotalScrollY = (int) ((float) mTotalScrollY + distanceY);
- if (!mCanLoop) {
- int initPositionCircleLength = (int) (mInitPosition * (mItemHeight));
- int initPositionStartY = -1 * initPositionCircleLength;
- if (mTotalScrollY < initPositionStartY) {
- mTotalScrollY = initPositionStartY;
- }
- int circleLength = (int) ((float) (mData.size() - 1 - mInitPosition) * (mItemHeight));
- if (mTotalScrollY >= circleLength) {
- mTotalScrollY = circleLength;
- }
- }
- invalidate();
- return true;
- }
- }
- class SelectedRunnable implements Runnable {
- @Override
- public final void run() {
- if (mListener != null) {
- mListener.onItemSelect(LoopView.this, getSelectedItem());
- }
- }
- }
- /**
- * Use in ACTION_UP
- */
- private class HalfHeightRunnable implements Runnable {
- int realTotalOffset;
- int realOffset;
- int offset;
- HalfHeightRunnable(int offset) {
- this.offset = offset;
- realTotalOffset = Integer.MAX_VALUE;
- realOffset = 0;
- }
- @Override
- public void run() {
- // first in
- if (realTotalOffset == Integer.MAX_VALUE) {
- if ((float) offset > mItemHeight / 2.0F) {
- // move to next item
- realTotalOffset = (int) (mItemHeight - (float) offset);
- } else {
- // move to pre item
- realTotalOffset = -offset;
- }
- }
- realOffset = (int) ((float) realTotalOffset * 0.1F);
- if (realOffset == 0) {
- if (realTotalOffset < 0) {
- realOffset = -1;
- } else {
- realOffset = 1;
- }
- }
- if (Math.abs(realTotalOffset) <= 0) {
- cancelSchedule();
- post(new Runnable() {
- @Override
- public void run() {
- if (mListener != null) {
- postDelayed(new SelectedRunnable(), 200L);
- }
- }
- });
- } else {
- mTotalScrollY = mTotalScrollY + realOffset;
- postInvalidate();
- realTotalOffset = realTotalOffset - realOffset;
- }
- }
- }
- /**
- * Use in {@link LoopViewGestureListener#onFling(MotionEvent, MotionEvent, float, float)}
- */
- private class FlingRunnable implements Runnable {
- float velocity;
- final float velocityY;
- FlingRunnable(float velocityY) {
- this.velocityY = velocityY;
- this.velocity = Integer.MAX_VALUE;
- }
- @Override
- public void run() {
- if (velocity == Integer.MAX_VALUE) {
- if (Math.abs(velocityY) > 2000F) {
- if (velocityY > 0.0F) {
- velocity = 2000F;
- } else {
- velocity = -2000F;
- }
- } else {
- velocity = velocityY;
- }
- }
- Log.i(TAG, "velocity->" + velocity);
- if (Math.abs(velocity) >= 0.0F && Math.abs(velocity) <= 20F) {
- cancelSchedule();
- post(new Runnable() {
- @Override
- public void run() {
- startSmoothScrollTo();
- }
- });
- return;
- }
- int i = (int) ((velocity * 10F) / 1000F);
- mTotalScrollY = mTotalScrollY - i;
- if (!mCanLoop) {
- float itemHeight = mLineSpacingMultiplier * mMaxTextHeight;
- if (mTotalScrollY <= (int) ((float) (-mInitPosition) * itemHeight)) {
- velocity = 40F;
- mTotalScrollY = (int) ((float) (-mInitPosition) * itemHeight);
- } else if (mTotalScrollY >= (int) ((float) (mData.size() - 1 - mInitPosition) * itemHeight)) {
- mTotalScrollY = (int) ((float) (mData.size() - 1 - mInitPosition) * itemHeight);
- velocity = -40F;
- }
- }
- if (velocity < 0.0F) {
- velocity = velocity + 20F;
- } else {
- velocity = velocity - 20F;
- }
- postInvalidate();
- }
- }
- public interface LoopScrollListener {
- void onItemSelect(LoopView loopView, int position);
- }
- }
|