Android使用Canvas 2D实现循环菜单效果

  public class LoopView extends View {

  private static final int MAX_VISIBLE_COUNT = 5;

  private TextPaint mTextPaint = null;

  private DisplayMetrics displayMetrics = null;

  private int mLineWidth = 1;

  private int mTextSize = 14;

  private int slopTouch = 0;

  private float circleRadius;

  private final List drawTasks = new ArrayList<>();

  private final List cacheDrawTasks = new ArrayList<>();

  private final List loopItems = new ArrayList<>();

  boolean isInit = false;

  private float startEventX = 0;

  private float startEventY = 0;

  private boolean isTouchMove = false;

  private float offsetY = 0;

  boolean isTouchEventUp = false;

  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);

  setClickable(true);

  setFocusable(true);

  setFocusableInTouchMode(true);

  displayMetrics = context.getResources().getDisplayMetrics();

  mTextPaint = createPaint();

  slopTouch = ViewConfiguration.get(context).getScaledTouchSlop();

  setLayerType(LAYER_TYPE_SOFTWARE, null);

  initDesignEditMode();

  }

  private void initDesignEditMode() {

  if (!isInEditMode()) return;

  int[] colors = {

  Color.RED,

  Color.CYAN,

  Color.YELLOW,

  Color.GRAY,

  Color.GREEN,

  Color.BLACK,

  Color.MAGENTA,

  0xffff9922,

  };

  String[] items = {

  "新闻",

  "科技",

  "历史",

  "军事",

  "小说",

  "娱乐",

  "电影",

  "电视剧",

  };

  for (int i = 0; i < items.length; i++) {

  LoopItem loopItem = new LoopItem(items[i], colors[i % colors.length]);

  loopItems.add(loopItem);

  }

  }

  @Override

  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

  super.onMeasure(widthMeasureSpec, heightMeasureSpec);

  int widthMode = MeasureSpec.getMode(widthMeasureSpec);

  int widthSize = MeasureSpec.getSize(widthMeasureSpec);

  if (widthMode != MeasureSpec.EXACTLY) {

  widthSize = displayMetrics.widthPixels;

  }

  int heightMode = MeasureSpec.getMode(heightMeasureSpec);

  int heightSize = MeasureSpec.getSize(heightMeasureSpec);

  if (heightMode != MeasureSpec.EXACTLY) {

  heightSize = (int) (displayMetrics.widthPixels * 0.9f);

  }

  setMeasuredDimension(widthSize, heightSize);

  }

  private TextPaint createPaint() {

  // 实例化画笔并打开抗锯齿

  TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);

  paint.setAntiAlias(true);

  paint.setStrokeWidth(dpTopx(mLineWidth));

  paint.setTextSize(dpTopx(mTextSize));

  return paint;

  }

  private float dpTopx(float dp) {

  return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());

  }

  /**

  * 基线到中线的距离=(Descent+Ascent)/2-Descent

  * 注意,实际获取到的Ascent是负数。公式推导过程如下:

  * 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。

  */

  public static float getTextPaintBaseline(Paint p) {

  Paint.FontMetrics fontMetrics = p.getFontMetrics();

  return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;

  }

  @Override

  protected void onSizeChanged(int w, int h, int oldw, int oldh) {

  super.onSizeChanged(w, h, oldw, oldh);

  circleRadius = Math.min(w / 2F, h / 2F) / MAX_VISIBLE_COUNT;

  }

  Comparator drawTaskComparator = new Comparator() {

  @Override

  public int compare(DrawTask left, DrawTask right) {

  float dx = Math.abs(right.y) - Math.abs(left.y);

  if (dx > 0) {

  return 1;

  }

  if (dx == 0) {

  return 0;

  }

  return -1;

  }

  };

  Comparator drawTaskComparatorY = new Comparator() {

  @Override

  public int compare(DrawTask left, DrawTask right) {

  float dx = left.y - right.y;

  if (dx > 0) {

  return 1;

  }

  if (dx == 0) {

  return 0;

  }

  return -1;

  }

  };

  @Override

  protected void onDraw(Canvas canvas) {

  super.onDraw(canvas);

  int width = getWidth();

  int height = getHeight();

  int id = canvas.save();

  canvas.translate(width / 2F, height / 2F);

  initCircle();

  //前期重置,以便recycler复用

  recyclerBefore(height);

  //复用和移除

  recycler();

  //再次处理,防止view复用之后产生其他位移

  recyclerAfter(height);

  Collections.sort(drawTasks, drawTaskComparator);

  for (int i = 0; i < drawTasks.size(); i++) {

  drawTasks.get(i).draw(canvas, mTextPaint);

  }

  drawGuideline(canvas, width);

  canvas.restoreToCount(id);

  }

  private float getMinYOffset() {

  float minY = 0;

  float offset = 0;

  for (int i = 0; i < drawTasks.size(); i++) {

  DrawTask drawTask = drawTasks.get(i);

  if (Math.abs(drawTask.scaleOffset) > offset) {

  minY = -drawTask.y;

  offset = drawTask.scaleOffset;

  }

  }

  return minY;

  }

  private void recyclerAfter(int height) {

  if (isTouchEventUp) {

  float centerOffset = getMinYOffset();

  resetItemYOffset(height, centerOffset);

  } else {

  resetItemYOffset(height, 0);

  }

  }

  private void recyclerBefore(int height) {

  if (isTouchEventUp) {

  float centerOffset = getMinYOffset();

  resetItemYOffset(height, centerOffset);

  } else {

  resetItemYOffset(height, offsetY);

  }

  isTouchEventUp = false;

  }

  private void recycler() {

  if (drawTasks.size() < (MAX_VISIBLE_COUNT - 2)) return;

  Collections.sort(drawTasks, drawTaskComparatorY);

  DrawTask head = drawTasks.get(0);

  DrawTask tail = drawTasks.get(drawTasks.size() - 1);

  int height = getHeight();

  if (head.y < -(height / 2F + circleRadius)) {

  drawTasks.remove(head);

  addToCachePool(head);

  head.setLoopItem(null);

  } else {

  DrawTask drawTask = getCachePool();

  LoopItem loopItem = head.getLoopItem();

  LoopItem preLoopItem = getPreLoopItem(loopItem);

  drawTask.setLoopItem(preLoopItem);

  drawTask.y = head.y - circleRadius * 2;

  drawTasks.add(0, drawTask);

  }

  if (tail.y > (height / 2F + circleRadius)) {

  drawTasks.remove(tail);

  addToCachePool(tail);

  tail.setLoopItem(null);

  } else {

  DrawTask drawTask = getCachePool();

  LoopItem loopItem = tail.getLoopItem();

  LoopItem nextLoopItem = getNextLoopItem(loopItem);

  drawTask.setLoopItem(nextLoopItem);

  drawTask.y = tail.y + circleRadius * 2;

  drawTasks.add(drawTask);

  }

  }

  private void resetItemYOffset(int height, float scaleOffset) {

  for (int i = 0; i < drawTasks.size(); i++) {

  DrawTask task = drawTasks.get(i);

  task.y = (task.y + scaleOffset);

  float ratio = Math.abs(task.y) / (height / 2F);

  if (ratio > 1f) {

  ratio = 1f;

  }

  task.scaleOffset = ((10 + circleRadius) * 3 / 4f) * (1 - ratio);

  }

  }

  RectF guideRect = new RectF();

  private void drawGuideline(Canvas canvas, int width) {

  if (!isInEditMode()) return;

  mTextPaint.setColor(Color.BLACK);

  mTextPaint.setStyle(Paint.Style.FILL);

  int i = 0;

  int counter = 0;

  while (counter < MAX_VISIBLE_COUNT) {

  float topY = i * 2 * circleRadius;

  guideRect.left = -width / 2f;

  guideRect.right = width / 2f;

  guideRect.top = topY - 0.5f;

  guideRect.bottom = topY + 0.5f;

  canvas.drawRect(guideRect, mTextPaint);

  counter++;

  float bottomY = -i * 2 * circleRadius;

  if (topY == bottomY) {

  i++;

  continue;

  }

  guideRect.top = bottomY - 0.5f;

  guideRect.bottom = bottomY + 0.5f;

  canvas.drawRect(guideRect, mTextPaint);

  counter++;

  i++;

  }

  }

  private LoopItem getNextLoopItem(LoopItem loopItem) {

  int index = loopItems.indexOf(loopItem);

  if (index < loopItems.size() - 1) {

  return loopItems.get(index + 1);

  }

  return loopItems.get(0);

  }

  private LoopItem getPreLoopItem(LoopItem loopItem) {

  int index = loopItems.indexOf(loopItem);

  if (index > 0) {

  return loopItems.get(index - 1);

  }

  return loopItems.get(loopItems.size() - 1);

  }

  private DrawTask getCachePool() {

  if (cacheDrawTasks.size() > 0) {

  return cacheDrawTasks.remove(0);

  }

  DrawTask drawTask = createDrawTask();

  return drawTask;

  }

  private void addToCachePool(DrawTask top) {

  cacheDrawTasks.add(top);

  }

  private void initCircle() {

  if (isInit) {

  return;

  }

  isInit = true;

  List drawTaskList = new ArrayList<>();

  int i = 0;

  while (drawTaskList.size() < MAX_VISIBLE_COUNT) {

  float topY = i * 2 * circleRadius;

  DrawTask drawTask = new DrawTask(0, topY, circleRadius);

  drawTaskList.add(drawTask);

  float bottomY = -i * 2 * circleRadius;

  if (topY == bottomY) {

  i++;

  continue;

  }

  drawTask = new DrawTask(0, bottomY, circleRadius);

  drawTaskList.add(drawTask);

  i++;

  }

  Collections.sort(drawTaskList, new Comparator() {

  @Override

  public int compare(DrawTask left, DrawTask right) {

  float dx = left.y - right.y;

  if (dx > 0) {

  return 1;

  }

  if (dx == 0) {

  return 0;

  }

  return -1;

  }

  });

  drawTasks.clear();

  if (loopItems.size() == 0) return;

  for (int j = 0; j < drawTaskList.size(); j++) {

  drawTaskList.get(j).setLoopItem(loopItems.get(j % loopItems.size()));

  }

  drawTasks.addAll(drawTaskList);

  }

  private DrawTask createDrawTask() {

  DrawTask drawTask = new DrawTask(0, 0, circleRadius);

  return drawTask;

  }

  @Override

  public boolean onTouchEvent(MotionEvent event) {

  int action = event.getActionMasked();

  isTouchEventUp = false;

  switch (action) {

  case MotionEvent.ACTION_DOWN:

  offsetY = 0;

  startEventX = event.getX() - getWidth() / 2F;

  startEventY = event.getY() - getHeight() / 2F;

  return true;

  case MotionEvent.ACTION_MOVE:

  float eventX = event.getX();

  float eventY = event.getY();

  if (eventY < 0) {

  eventY = 0;

  }

  if (eventX < 0) {

  eventX = 0;

  }

  if (eventY > getWidth()) {

  eventX = getWidth();

  }

  if (eventY > getHeight()) {

  eventY = getHeight();

  }

  float currentX = eventX - getWidth() / 2F;

  float currentY = eventY - getHeight() / 2F;

  float dx = currentX - startEventX;

  float dy = currentY - startEventY;

  if (Math.abs(dx) < Math.abs(dy) && Math.abs(dy) >= slopTouch) {

  isTouchMove = true;

  }

  if (!isTouchMove) {

  break;

  }

  offsetY = dy;

  startEventX = currentX;

  startEventY = currentY;

  postInvalidate();

  return true;

  case MotionEvent.ACTION_CANCEL:

  case MotionEvent.ACTION_OUTSIDE:

  case MotionEvent.ACTION_UP:

  isTouchMove = false;

  isTouchEventUp = true;

  offsetY = 0;

  Log.d("eventup", "offsetY=" + offsetY);

  invalidate();

  break;

  }

  return super.onTouchEvent(event);

  }

  public void setLoopItems(List loopItems) {

  this.loopItems.clear();

  this.drawTasks.clear();

  this.cacheDrawTasks.clear();

  this.isInit = false;

  if (loopItems != null) {

  this.loopItems.addAll(loopItems);

  }

  postInvalidate();

  }

  public static class DrawTask {

  private T loopItem;

  private float radius;

  private float x;

  private float y;

  private float scaleOffset = 0;

  public DrawTask(float x, float y, float radius) {

  this.radius = radius;

  this.x = x;

  this.y = y;

  }

  public void setLoopItem(T loopItem) {

  this.loopItem = loopItem;

  }

  public void draw(Canvas canvas, TextPaint textPaint) {

  if (loopItem == null) return;

  textPaint.setColor(loopItem.getColor());

  textPaint.setStyle(Paint.Style.FILL);

  textPaint.setShadowLayer(10, 0, 5, 0x99444444);

  canvas.drawCircle(x, y, radius + scaleOffset, textPaint);

  textPaint.setColor(Color.WHITE);

  textPaint.setStyle(Paint.Style.FILL);

  String text = loopItem.getText();

  float textWidth = textPaint.measureText(text);

  float baseline = getTextPaintBaseline(textPaint);

  textPaint.setShadowLayer(0, 0, 0, Color.TRANSPARENT);

  canvas.drawText(text, -textWidth / 2, y + baseline, textPaint);

  }

  public T getLoopItem() {

  return loopItem;

  }

  }

  public static class LoopItem {

  private int color;

  private String text;

  public LoopItem(String text, int color) {

  this.color = color;

  this.text = text;

  }

  public int getColor() {

  return color;

  }

  public void setColor(int color) {

  this.color = color;

  }

  public String getText() {

  return text;

  }

  public void setText(String text) {

  this.text = text;

  }

  }

  }