CircleLayoutManager實現圓形RecyclerView

public class CircleLayoutManager extends RecyclerView.LayoutManager {
  /**
   * 默認每個item之間的角度
   **/
  private static float INTERVAL_ANGLE = 22.5f;
  /**
   * 滑動距離和角度的一個比例
   **/
  private static float DISTANCE_RATIO = 20f;
  /**
   * 默認的半徑長度
   **/
  private static final int DEFAULT_RADIO = 100;
  /**
   * 滑動的方向
   */
  private static int SCROLL_LEFT = 1;
  private static int SCROLL_RIGHT = 2;
  /**
   * 半徑默認為100
   **/
  private int mRadius;
  /**
   * 當前旋轉的角度
   **/
  private float offsetRotate;
  private int startLeft;
  private int startTop;
  /**
   * 第一個的角度是為0
   **/
  private int firstChildRotate = 0;
  //每個item之間的角度間隔
  private float intervalAngle;
  //最大和最小的移除角度
  private int minRemoveDegree;
  private int maxRemoveDegree;
  //記錄Item是否出現過屏幕且還沒有回收。true表示出現過屏幕上牙瓢,并且還沒被回收
  private SparseBooleanArray itemAttached = new SparseBooleanArray();
  //保存所有的Item的上下左右的偏移量信息
  private SparseArray<Float> itemsRotate = new SparseArray<>();
  // 這里的每個item的大小都是一樣的
  private int mDecoratedChildWidth;
  private int mDecoratedChildHeight;

  public CircleLayoutManager() {
    this(DEFAULT_RADIO, 5);
  }

  public CircleLayoutManager(int mRadius, int showCount) {
    this.mRadius = mRadius;
    offsetRotate = 0;
    INTERVAL_ANGLE = 90.0f / (showCount - 1);
    intervalAngle = INTERVAL_ANGLE;
    minRemoveDegree = 180;
    maxRemoveDegree = 270;
    firstChildRotate = minRemoveDegree;
  }

  @Override
  public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
  }

  @Override
  public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //如果沒有item劫拗,直接返回
    //跳過preLayout,preLayout主要用于支持動畫
    if (state.getItemCount() <= 0 || state.isPreLayout()) {
      offsetRotate = 0;
      if (state.getItemCount() == 0) {
        removeAndRecycleAllViews(recycler);
      }
      return;
    }
    //得到子view的寬和高矾克,這邊的item的寬高都是一樣的页慷,所以只需要進行一次測量
    View scrap = recycler.getViewForPosition(0);
    addView(scrap);
    measureChildWithMargins(scrap, 0, 0);
    //計算測量布局的寬高
    mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
    mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
    //確定起始位置,在右下角
    startLeft = getHorizontalSpace() - mDecoratedChildWidth;
    startTop = getVerticalSpace() - mDecoratedChildHeight;
    //記錄每個item旋轉的角度
    float rotate = firstChildRotate;
    for (int i = 0; i < getItemCount(); i++) {
      itemsRotate.put(i, rotate);
      itemAttached.put(i, false);
      rotate += intervalAngle;
    }
    //在布局之前,將所有的子View先Detach掉酒繁,放入到Scrap緩存中
    detachAndScrapAttachedViews(recycler);
    fixRotateOffset();
    layoutItems(recycler, state);
  }

  /**
   * 進行view的回收和顯示
   **/
  private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state) {
    layoutItems(recycler, state, SCROLL_RIGHT);
  }

  /**
   * 進行view的回收和顯示的具體實現
   **/
  private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state, int oritention) {
    if (state.isPreLayout()) return;
    //移除界面之外的view
    for (int i = 0; i < getChildCount(); i++) {
      View view = getChildAt(i);
      int position = getPosition(view);
      if (itemsRotate.get(position) - offsetRotate > maxRemoveDegree || itemsRotate.get(position) - offsetRotate < minRemoveDegree) {
        itemAttached.put(position, false);
        removeAndRecycleView(view, recycler);
      }
    }
    //將要顯示的view進行顯示出來
    int count = getItemCount();
    for (int i = 0; i < count; i++) {
      if (itemsRotate.get(i) - offsetRotate <= maxRemoveDegree + INTERVAL_ANGLE && itemsRotate.get(i) - offsetRotate >= minRemoveDegree - INTERVAL_ANGLE) {
        if (!itemAttached.get(i)) {
          ViewGroup scrap = (ViewGroup) recycler.getViewForPosition(i);
          View childView = scrap.getChildAt(0);
          measureChildWithMargins(scrap, 0, 0);
          if (oritention == SCROLL_LEFT) {
            addView(scrap, 0);
          } else {
            addView(scrap);
          }
          float rotate = itemsRotate.get(i);
          if (count > 90 / INTERVAL_ANGLE + 1) {
            rotate -= offsetRotate;
          }
          int left = calLeftPosition(rotate);
          int top = calTopPosition(rotate);
          scrap.setRotation(rotate);
          layoutDecorated(scrap, startLeft + left, startTop + top, startLeft + left + mDecoratedChildWidth, startTop + top + mDecoratedChildHeight);
          childView.setRotation(-rotate);
          itemAttached.put(i, true);
        }
      }
    }
  }

  @Override
  public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (90.0f / INTERVAL_ANGLE + 1 >= getItemCount()) {
      return 0;
    }
    int willScroll = -dx;
    //每個item x方向上的移動距離
    float theta = -dx / DISTANCE_RATIO;
    float targetRotate = offsetRotate + theta;
    //目標角度
    if (targetRotate < 0) {
      willScroll = (int) (-offsetRotate * DISTANCE_RATIO);
    } else if (targetRotate > getMaxOffsetDegree()) {
      willScroll = (int) ((getMaxOffsetDegree() - offsetRotate) * DISTANCE_RATIO);
    }
    theta = willScroll / DISTANCE_RATIO;
    //當前移動的總角度
    offsetRotate += theta;
    //重新設置每個item的x和y的坐標
    for (int i = 0; i < getChildCount(); i++) {
      ViewGroup view = (ViewGroup) getChildAt(i);
      View childView = view.getChildAt(0);
      float newRotate = view.getRotation() - theta;
      int offsetX = calLeftPosition(newRotate);
      int offsetY = calTopPosition(newRotate);
      view.setRotation(newRotate);
      layoutDecorated(view, startLeft + offsetX, startTop + offsetY, startLeft + offsetX + mDecoratedChildWidth, startTop + offsetY + mDecoratedChildHeight);
      childView.setRotation(-newRotate);
    }
    //根據dx的大小判斷是左滑還是右滑
    if (dx > 0) {
      layoutItems(recycler, state, SCROLL_LEFT);
    } else {
      layoutItems(recycler, state, SCROLL_RIGHT);
    }
    return willScroll;
  }

  @Override
  public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    return scrollHorizontallyBy(-dy, recycler, state);
  }

  @Override
  public boolean canScrollHorizontally() {
    return true;
  }

  @Override
  public boolean canScrollVertically() {
    return true;
  }

  /**
   * 當前item的x的坐標
   **/
  private int calLeftPosition(float rotate) {
    return (int) (mRadius * Math.cos(Math.toRadians(90 - rotate)));
  }

  /**
   * 當前item的y的坐標
   **/
  private int calTopPosition(float rotate) {
    return (int) (mRadius * Math.sin(Math.toRadians(90 - rotate)));
  }

  /**
   * 設置滾動時候的角度
   **/
  private void fixRotateOffset() {
    if (offsetRotate < 0) {
      offsetRotate = 0;
    }
    if (offsetRotate > getMaxOffsetDegree()) {
      offsetRotate = getMaxOffsetDegree();
    }
  }

  /**
   * 最大的角度
   **/
  private float getMaxOffsetDegree() {
    return (getItemCount() - 1) * intervalAngle - 90;
  }

  private int getHorizontalSpace() {
    return getWidth() - getPaddingRight() - getPaddingLeft();
  }

  private int getVerticalSpace() {
    return getHeight() - getPaddingBottom() - getPaddingTop();
  }


  private PointF computeScrollVectorForPosition(int targetPosition) {
    if (getChildCount() == 0) {
      return null;
    }
    final int firstChildPos = getPosition(getChildAt(0));
    final int direction = targetPosition < firstChildPos ? -1 : 1;
    return new PointF(direction, 0);
  }

  @Override
  public void scrollToPosition(int position) {//移動到某一項
    if (position < 0 || position > getItemCount() - 1) return;
    float targetRotate = position * intervalAngle;
    if (targetRotate == offsetRotate) return;
    offsetRotate = targetRotate;
    fixRotateOffset();
    requestLayout();
  }

  @Override
  public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {//平滑的移動到某一項
    LinearSmoothScroller smoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
      @Override
      public PointF computeScrollVectorForPosition(int targetPosition) {
        return CircleLayoutManager.this.computeScrollVectorForPosition(targetPosition);
      }
    };
    smoothScroller.setTargetPosition(position);
    startSmoothScroll(smoothScroller);
  }

  @Override
  public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {//adapter進行改變的時候
    removeAllViews();
    offsetRotate = 0;
  }
}

RecyclerView的布局文件如下:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="80dp"
    android:layout_height="80dp">
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <ImageView
            android:id="@+id/item_img"
            android:layout_width="35dp"
            android:layout_gravity="center_horizontal"
            android:layout_height="35dp"
            android:padding="2dp"
            android:scaleType="centerCrop" />
        <TextView
            android:id="@+id/item_text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:textSize="12sp"
            android:textColor="#FFF"
            android:gravity="center_horizontal"/>
    </LinearLayout>
</FrameLayout>
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末滓彰,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子州袒,更是在濱河造成了極大的恐慌揭绑,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件郎哭,死亡現場離奇詭異洗做,居然都是意外死亡,警方通過查閱死者的電腦和手機彰居,發(fā)現死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撰筷,“玉大人陈惰,你說我怎么就攤上這事”献眩” “怎么了抬闯?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長关筒。 經常有香客問我溶握,道長,這世上最難降的妖魔是什么蒸播? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任睡榆,我火速辦了婚禮,結果婚禮上袍榆,老公的妹妹穿的比我還像新娘胀屿。我一直安慰自己,他們只是感情好包雀,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布宿崭。 她就那樣靜靜地躺著,像睡著了一般才写。 火紅的嫁衣襯著肌膚如雪葡兑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天赞草,我揣著相機與錄音讹堤,去河邊找鬼。 笑死房资,一個胖子當著我的面吹牛蜕劝,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岖沛,長吁一口氣:“原來是場噩夢啊……” “哼暑始!你這毒婦竟也來了?” 一聲冷哼從身側響起婴削,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤廊镜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后唉俗,有當地人在樹林里發(fā)現了一具尸體嗤朴,經...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年虫溜,在試婚紗的時候發(fā)現自己被綠了雹姊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡衡楞,死狀恐怖吱雏,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情瘾境,我是刑警寧澤歧杏,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站迷守,受9級特大地震影響犬绒,放射性物質發(fā)生泄漏。R本人自食惡果不足惜兑凿,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一略水、第九天 我趴在偏房一處隱蔽的房頂上張望苦酱。 院中可真熱鬧葱淳,春花似錦辆飘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至晨雳,卻和暖如春行瑞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背餐禁。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工血久, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人帮非。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓氧吐,卻偏偏與公主長得像讹蘑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子筑舅,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內容