a2c39b5444abd707bb5d242f60c598f81d7d13a5.svn-base 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. package com.hjq.dialog.widget;
  2. import android.annotation.SuppressLint;
  3. import android.content.Context;
  4. import android.content.res.TypedArray;
  5. import android.graphics.Canvas;
  6. import android.graphics.Paint;
  7. import android.graphics.Rect;
  8. import android.graphics.Typeface;
  9. import android.util.AttributeSet;
  10. import android.util.Log;
  11. import android.util.TypedValue;
  12. import android.view.GestureDetector;
  13. import android.view.MotionEvent;
  14. import android.view.View;
  15. import com.hjq.dialog.R;
  16. import java.util.List;
  17. import java.util.concurrent.Executors;
  18. import java.util.concurrent.ScheduledExecutorService;
  19. import java.util.concurrent.ScheduledFuture;
  20. import java.util.concurrent.TimeUnit;
  21. /**
  22. * desc : 循环滚动列表自定义控件
  23. */
  24. public final class LoopView extends View {
  25. private static final String TAG = "LoopView";
  26. private ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor();
  27. private ScheduledFuture mScheduledFuture;
  28. private LoopScrollListener mListener;
  29. private List<String> mData;
  30. private Paint mTopBottomTextPaint;
  31. private Paint mCenterTextPaint;
  32. private Paint mCenterLinePaint;
  33. private int mTotalScrollY;
  34. private GestureDetector mGestureDetector;
  35. private int mSelectedItem;
  36. private int mTextSize;
  37. private int mMaxTextWidth;
  38. private int mMaxTextHeight;
  39. private int mTopBottomTextColor;
  40. private int mCenterTextColor;
  41. private int mCenterLineColor;
  42. private float mLineSpacingMultiplier;
  43. private boolean mCanLoop;
  44. private float mTopLineY;
  45. private float mBottomLineY;
  46. private int mCurrentIndex;
  47. private int mInitPosition;
  48. private float mHorizontalPadding;
  49. private float mVerticalPadding;
  50. private float mItemHeight;
  51. private int mDrawItemsCount;
  52. private String[] mItemTempArray;
  53. private float mCircularDiameter;
  54. private float mCircularRadius;
  55. public LoopView(Context context) {
  56. this(context, null);
  57. }
  58. public LoopView(Context context, AttributeSet attrs) {
  59. this(context, attrs, 0);
  60. }
  61. public LoopView(Context context, AttributeSet attrs, int defStyleAttr) {
  62. super(context, attrs, defStyleAttr);
  63. TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LoopView);
  64. if (array != null) {
  65. mTopBottomTextColor = array.getColor(R.styleable.LoopView_lv_topBottomTextColor, 0xffafafaf);
  66. mCenterTextColor = array.getColor(R.styleable.LoopView_lv_centerTextColor, 0xff313131);
  67. mCenterLineColor = array.getColor(R.styleable.LoopView_lv_lineColor, 0xffc5c5c5);
  68. mCanLoop = array.getBoolean(R.styleable.LoopView_lv_canLoop, true);
  69. mInitPosition = array.getInt(R.styleable.LoopView_lv_initPosition, -1);
  70. mTextSize = array.getDimensionPixelSize(R.styleable.LoopView_lv_textSize, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18, getContext().getResources().getDisplayMetrics()));
  71. mDrawItemsCount = array.getInt(R.styleable.LoopView_lv_drawItemCount, 7);
  72. mItemTempArray = new String[mDrawItemsCount];
  73. array.recycle();
  74. }
  75. mLineSpacingMultiplier = 3;
  76. mTopBottomTextPaint = new Paint();
  77. mCenterTextPaint = new Paint();
  78. mCenterLinePaint = new Paint();
  79. setLayerType(LAYER_TYPE_SOFTWARE, null);
  80. mGestureDetector = new GestureDetector(context, new LoopViewGestureListener());
  81. mGestureDetector.setIsLongpressEnabled(false);
  82. }
  83. @Override
  84. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  85. switch (MeasureSpec.getMode(widthMeasureSpec)) {
  86. case MeasureSpec.AT_MOST:
  87. case MeasureSpec.UNSPECIFIED:
  88. widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxTextWidth, MeasureSpec.EXACTLY);
  89. break;
  90. case MeasureSpec.EXACTLY:
  91. break;
  92. }
  93. switch (MeasureSpec.getMode(heightMeasureSpec)) {
  94. case MeasureSpec.AT_MOST:
  95. case MeasureSpec.UNSPECIFIED:
  96. heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) mCircularDiameter, MeasureSpec.EXACTLY);
  97. break;
  98. case MeasureSpec.EXACTLY:
  99. break;
  100. }
  101. setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
  102. int width = MeasureSpec.getSize(widthMeasureSpec);
  103. int height = MeasureSpec.getSize(heightMeasureSpec);
  104. mItemHeight = mLineSpacingMultiplier * mMaxTextHeight;
  105. // auto calculate the text's left/right value when draw
  106. mHorizontalPadding = (width - mMaxTextWidth) / 2;
  107. mVerticalPadding = (height - mCircularDiameter) / 2;
  108. // topLineY = diameter/2 - itemHeight(mItemHeight) / 2 + mVerticalPadding
  109. mTopLineY = ((mCircularDiameter - mItemHeight) / 2) + mVerticalPadding;
  110. mBottomLineY = ((mCircularDiameter + mItemHeight) / 2) + mVerticalPadding;
  111. }
  112. @Override
  113. protected void onDraw(Canvas canvas) {
  114. if (mData == null) return;
  115. // the length of single item is mItemHeight
  116. int mChangingItem = (int) (mTotalScrollY / (mItemHeight));
  117. mCurrentIndex = mInitPosition + mChangingItem % mData.size();
  118. if (!mCanLoop) { // can loop
  119. if (mCurrentIndex < 0) {
  120. mCurrentIndex = 0;
  121. }
  122. if (mCurrentIndex > mData.size() - 1) {
  123. mCurrentIndex = mData.size() - 1;
  124. }
  125. } else { // can not loop
  126. if (mCurrentIndex < 0) {
  127. mCurrentIndex = mData.size() + mCurrentIndex;
  128. }
  129. if (mCurrentIndex > mData.size() - 1) {
  130. mCurrentIndex = mCurrentIndex - mData.size();
  131. }
  132. }
  133. int count = 0;
  134. // reconfirm each item's value from dataList according to currentIndex,
  135. while (count < mDrawItemsCount) {
  136. int templateItem = mCurrentIndex - (mDrawItemsCount / 2 - count);
  137. if (mCanLoop) {
  138. if (templateItem < 0) {
  139. templateItem = templateItem + mData.size();
  140. }
  141. if (templateItem > mData.size() - 1) {
  142. templateItem = templateItem - mData.size();
  143. }
  144. mItemTempArray[count] = mData.get(templateItem);
  145. } else if (templateItem < 0) {
  146. mItemTempArray[count] = "";
  147. } else if (templateItem > mData.size() - 1) {
  148. mItemTempArray[count] = "";
  149. } else {
  150. mItemTempArray[count] = mData.get(templateItem);
  151. }
  152. count++;
  153. }
  154. // draw top and bottom line
  155. canvas.drawLine(0, mTopLineY, getMeasuredWidth(), mTopLineY, mCenterLinePaint);
  156. canvas.drawLine(0, mBottomLineY, getMeasuredWidth(), mBottomLineY, mCenterLinePaint);
  157. count = 0;
  158. int changingLeftY = (int) (mTotalScrollY % (mItemHeight));
  159. while (count < mDrawItemsCount) {
  160. canvas.save();
  161. // L= å * r -> å = rad
  162. float itemHeight = mMaxTextHeight * mLineSpacingMultiplier;
  163. // get radian L = (itemHeight * count - changingLeftY),r = mCircularRadius
  164. double radian = (itemHeight * count - changingLeftY) / mCircularRadius;
  165. // a = rad * 180 / π
  166. // get angle
  167. float angle = (float) (radian * 180 / Math.PI);
  168. // when angle >= 180 || angle <= 0 don't draw
  169. if (angle >= 180F || angle <= 0F) {
  170. canvas.restore();
  171. } else {
  172. // translateY = r - r*cos(å) -
  173. // (Math.sin(radian) * mMaxTextHeight) / 2 this is text offset
  174. float translateY = (float) (mCircularRadius - Math.cos(radian) * mCircularRadius - (Math.sin(radian) * mMaxTextHeight) / 2) + mVerticalPadding;
  175. canvas.translate(0.0F, translateY);
  176. // scale offset = Math.sin(radian) -> 0 - 1
  177. canvas.scale(1.0F, (float) Math.sin(radian));
  178. if (translateY <= mTopLineY) {
  179. // draw text y between 0 -> mTopLineY,include incomplete text
  180. canvas.save();
  181. canvas.clipRect(0, 0, getMeasuredWidth(), mTopLineY - translateY);
  182. canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mTopBottomTextPaint);
  183. canvas.restore();
  184. canvas.save();
  185. canvas.clipRect(0, mTopLineY - translateY, getMeasuredWidth(), (int) (itemHeight));
  186. canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mCenterTextPaint);
  187. canvas.restore();
  188. } else if (mMaxTextHeight + translateY >= mBottomLineY) {
  189. // draw text y between mTopLineY -> mBottomLineY ,include incomplete text
  190. canvas.save();
  191. canvas.clipRect(0, 0, getMeasuredWidth(), mBottomLineY - translateY);
  192. canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mCenterTextPaint);
  193. canvas.restore();
  194. canvas.save();
  195. canvas.clipRect(0, mBottomLineY - translateY, getMeasuredWidth(), (int) (itemHeight));
  196. canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mTopBottomTextPaint);
  197. canvas.restore();
  198. } else if (translateY >= mTopLineY && mMaxTextHeight + translateY <= mBottomLineY) {
  199. // draw center complete text
  200. canvas.clipRect(0, 0, getMeasuredWidth(), (int) (itemHeight));
  201. canvas.drawText(mItemTempArray[count], mHorizontalPadding, mMaxTextHeight, mCenterTextPaint);
  202. // center one indicate selected item
  203. mSelectedItem = mData.indexOf(mItemTempArray[count]);
  204. }
  205. canvas.restore();
  206. }
  207. count++;
  208. }
  209. }
  210. @SuppressLint("ClickableViewAccessibility")
  211. @Override
  212. public boolean onTouchEvent(MotionEvent motionevent) {
  213. switch (motionevent.getAction()) {
  214. case MotionEvent.ACTION_UP:
  215. default:
  216. if (!mGestureDetector.onTouchEvent(motionevent)) {
  217. startSmoothScrollTo();
  218. }
  219. }
  220. return true;
  221. }
  222. public final void setCanLoop(boolean canLoop) {
  223. mCanLoop = canLoop;
  224. invalidate();
  225. }
  226. /**
  227. * set text size
  228. *
  229. * @param size size indicate sp,not px
  230. */
  231. public final void setTextSize(float size) {
  232. if (size > 0) {
  233. mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, getContext().getResources().getDisplayMetrics());
  234. }
  235. }
  236. public void setLineSpacingMultiplier(float spacing) {
  237. this.mLineSpacingMultiplier = spacing;
  238. }
  239. public int getSelectedItem() {
  240. return mSelectedItem;
  241. }
  242. public void setInitPosition(int initPosition) {
  243. if (mData == null) return;
  244. if (initPosition > mData.size()) {
  245. initPosition = mData.size() - 1;
  246. }
  247. mInitPosition = initPosition;
  248. invalidate();
  249. if (mListener != null) {
  250. mListener.onItemSelect(this, initPosition);
  251. }
  252. }
  253. public void setLoopListener(LoopScrollListener l) {
  254. mListener = l;
  255. }
  256. /**
  257. * All public method must be called before this method
  258. * @param data data list
  259. */
  260. public final void setData(List<String> data) {
  261. mData = data;
  262. if (mData == null) {
  263. throw new IllegalArgumentException("data list must not be null!");
  264. }
  265. mTopBottomTextPaint.setColor(mTopBottomTextColor);
  266. mTopBottomTextPaint.setAntiAlias(true);
  267. mTopBottomTextPaint.setTypeface(Typeface.MONOSPACE);
  268. mTopBottomTextPaint.setTextSize(mTextSize);
  269. mCenterTextPaint.setColor(mCenterTextColor);
  270. mCenterTextPaint.setAntiAlias(true);
  271. mCenterTextPaint.setTextScaleX(1.05F);
  272. mCenterTextPaint.setTypeface(Typeface.MONOSPACE);
  273. mCenterTextPaint.setTextSize(mTextSize);
  274. mCenterLinePaint.setColor(mCenterLineColor);
  275. mCenterLinePaint.setAntiAlias(true);
  276. mCenterLinePaint.setTypeface(Typeface.MONOSPACE);
  277. mCenterLinePaint.setTextSize(mTextSize);
  278. // measureTextWidthHeight
  279. Rect rect = new Rect();
  280. for (int i = 0; i < mData.size(); i++) {
  281. String text = mData.get(i);
  282. mCenterTextPaint.getTextBounds(text, 0, text.length(), rect);
  283. int textWidth = rect.width();
  284. if (textWidth > mMaxTextWidth) {
  285. mMaxTextWidth = textWidth;
  286. }
  287. //int textHeight = rect.height();
  288. //if (textHeight > mMaxTextHeight) {
  289. // mMaxTextHeight = textHeight;
  290. //}
  291. Paint.FontMetrics fontMetrics = mCenterTextPaint.getFontMetrics();
  292. mMaxTextHeight = (int) (fontMetrics.bottom - fontMetrics.top) * 2 / 3;
  293. }
  294. // 计算半圆周 -- mMaxTextHeight * mLineSpacingMultiplier 表示每个item的高度 mDrawItemsCount = 7
  295. // 实际显示5个,留两个是在圆周的上下面
  296. // lineSpacingMultiplier是指text上下的距离的值和maxTextHeight一样的意思 所以 = 2
  297. // mDrawItemsCount - 1 代表圆周的上下两面各被剪切了一半 相当于高度少了一个 mMaxTextHeight
  298. int halfCircumference = (int) (mMaxTextHeight * mLineSpacingMultiplier * (mDrawItemsCount - 1));
  299. // the diameter of circular 2πr = cir, 2r = height
  300. mCircularDiameter = (int) ((halfCircumference * 2) / Math.PI);
  301. // the radius of circular
  302. mCircularRadius = (int) (halfCircumference / Math.PI);
  303. // FIXME: 7/8/16 通过控件的高度来计算圆弧的周长
  304. if (mInitPosition == -1) {
  305. if (mCanLoop) {
  306. mInitPosition = (mData.size() + 1) / 2;
  307. } else {
  308. mInitPosition = 0;
  309. }
  310. }
  311. mCurrentIndex = mInitPosition;
  312. invalidate();
  313. }
  314. private void cancelSchedule() {
  315. if (mScheduledFuture != null && !mScheduledFuture.isCancelled()) {
  316. mScheduledFuture.cancel(true);
  317. mScheduledFuture = null;
  318. }
  319. }
  320. private void startSmoothScrollTo() {
  321. int offset = (int) (mTotalScrollY % (mItemHeight));
  322. cancelSchedule();
  323. mScheduledFuture = mExecutor.scheduleWithFixedDelay(new HalfHeightRunnable(offset), 0, 10, TimeUnit.MILLISECONDS);
  324. }
  325. private void startSmoothScrollTo(float velocityY) {
  326. cancelSchedule();
  327. int velocityFling = 20;
  328. mScheduledFuture = mExecutor.scheduleWithFixedDelay(new FlingRunnable(velocityY), 0, velocityFling, TimeUnit.MILLISECONDS);
  329. }
  330. class LoopViewGestureListener extends android.view.GestureDetector.SimpleOnGestureListener {
  331. @Override
  332. public final boolean onDown(MotionEvent motionevent) {
  333. cancelSchedule();
  334. Log.i(TAG, "LoopViewGestureListener->onDown");
  335. return true;
  336. }
  337. @Override
  338. public final boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
  339. startSmoothScrollTo(velocityY);
  340. Log.i(TAG, "LoopViewGestureListener->onFling");
  341. return true;
  342. }
  343. @Override
  344. public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
  345. Log.i(TAG, "LoopViewGestureListener->onScroll");
  346. mTotalScrollY = (int) ((float) mTotalScrollY + distanceY);
  347. if (!mCanLoop) {
  348. int initPositionCircleLength = (int) (mInitPosition * (mItemHeight));
  349. int initPositionStartY = -1 * initPositionCircleLength;
  350. if (mTotalScrollY < initPositionStartY) {
  351. mTotalScrollY = initPositionStartY;
  352. }
  353. int circleLength = (int) ((float) (mData.size() - 1 - mInitPosition) * (mItemHeight));
  354. if (mTotalScrollY >= circleLength) {
  355. mTotalScrollY = circleLength;
  356. }
  357. }
  358. invalidate();
  359. return true;
  360. }
  361. }
  362. class SelectedRunnable implements Runnable {
  363. @Override
  364. public final void run() {
  365. if (mListener != null) {
  366. mListener.onItemSelect(LoopView.this, getSelectedItem());
  367. }
  368. }
  369. }
  370. /**
  371. * Use in ACTION_UP
  372. */
  373. private class HalfHeightRunnable implements Runnable {
  374. int realTotalOffset;
  375. int realOffset;
  376. int offset;
  377. HalfHeightRunnable(int offset) {
  378. this.offset = offset;
  379. realTotalOffset = Integer.MAX_VALUE;
  380. realOffset = 0;
  381. }
  382. @Override
  383. public void run() {
  384. // first in
  385. if (realTotalOffset == Integer.MAX_VALUE) {
  386. if ((float) offset > mItemHeight / 2.0F) {
  387. // move to next item
  388. realTotalOffset = (int) (mItemHeight - (float) offset);
  389. } else {
  390. // move to pre item
  391. realTotalOffset = -offset;
  392. }
  393. }
  394. realOffset = (int) ((float) realTotalOffset * 0.1F);
  395. if (realOffset == 0) {
  396. if (realTotalOffset < 0) {
  397. realOffset = -1;
  398. } else {
  399. realOffset = 1;
  400. }
  401. }
  402. if (Math.abs(realTotalOffset) <= 0) {
  403. cancelSchedule();
  404. post(new Runnable() {
  405. @Override
  406. public void run() {
  407. if (mListener != null) {
  408. postDelayed(new SelectedRunnable(), 200L);
  409. }
  410. }
  411. });
  412. } else {
  413. mTotalScrollY = mTotalScrollY + realOffset;
  414. postInvalidate();
  415. realTotalOffset = realTotalOffset - realOffset;
  416. }
  417. }
  418. }
  419. /**
  420. * Use in {@link LoopViewGestureListener#onFling(MotionEvent, MotionEvent, float, float)}
  421. */
  422. private class FlingRunnable implements Runnable {
  423. float velocity;
  424. final float velocityY;
  425. FlingRunnable(float velocityY) {
  426. this.velocityY = velocityY;
  427. this.velocity = Integer.MAX_VALUE;
  428. }
  429. @Override
  430. public void run() {
  431. if (velocity == Integer.MAX_VALUE) {
  432. if (Math.abs(velocityY) > 2000F) {
  433. if (velocityY > 0.0F) {
  434. velocity = 2000F;
  435. } else {
  436. velocity = -2000F;
  437. }
  438. } else {
  439. velocity = velocityY;
  440. }
  441. }
  442. Log.i(TAG, "velocity->" + velocity);
  443. if (Math.abs(velocity) >= 0.0F && Math.abs(velocity) <= 20F) {
  444. cancelSchedule();
  445. post(new Runnable() {
  446. @Override
  447. public void run() {
  448. startSmoothScrollTo();
  449. }
  450. });
  451. return;
  452. }
  453. int i = (int) ((velocity * 10F) / 1000F);
  454. mTotalScrollY = mTotalScrollY - i;
  455. if (!mCanLoop) {
  456. float itemHeight = mLineSpacingMultiplier * mMaxTextHeight;
  457. if (mTotalScrollY <= (int) ((float) (-mInitPosition) * itemHeight)) {
  458. velocity = 40F;
  459. mTotalScrollY = (int) ((float) (-mInitPosition) * itemHeight);
  460. } else if (mTotalScrollY >= (int) ((float) (mData.size() - 1 - mInitPosition) * itemHeight)) {
  461. mTotalScrollY = (int) ((float) (mData.size() - 1 - mInitPosition) * itemHeight);
  462. velocity = -40F;
  463. }
  464. }
  465. if (velocity < 0.0F) {
  466. velocity = velocity + 20F;
  467. } else {
  468. velocity = velocity - 20F;
  469. }
  470. postInvalidate();
  471. }
  472. }
  473. public interface LoopScrollListener {
  474. void onItemSelect(LoopView loopView, int position);
  475. }
  476. }