/* * Copyright 2017 Sun Jian * <p> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.crazysunj.cardslideview; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SnapHelper; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; /** * @author sunjian * @date 2019-07-19 14:21 */ public class CardSlideView<T> extends FrameLayout { public static final int HORIZONTAL = LinearLayout.HORIZONTAL; public static final int VERTICAL = LinearLayout.VERTICAL; @Retention(RetentionPolicy.SOURCE) @IntDef({HORIZONTAL, VERTICAL}) public @interface Orientation { } private static final float MAX_OFFSET_PERCENT = 1.f; private static final float MIN_OFFSET_PERCENT = 0f; /** * 除了中间固定的一个外,两侧的其中一侧偏移量百分比,因为是中心对称,只要考虑一边就行了 * 默认0.4f,范围是0~1 */ private float mSideOffsetPercent; /** * 是否无限循环 * 默认非无限循环 */ private boolean mIsLoop; /** * item的宽高比,高:宽 */ private float mItemRate; private InnerRecyclerView mCardListView; private GalleryLayoutManager mLayoutManager; private CardAdapter<T> mHorizontalAdapter; private CardAdapter<T> mVerticalAdapter; public CardSlideView(@NonNull Context context) { super(context); initView(context, null); } public CardSlideView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); initView(context, attrs); } public CardSlideView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context, attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public CardSlideView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initView(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (getOrientation() == HORIZONTAL) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { throw new RuntimeException("水平方向,宽度必须固定,可以设置MATCH_PARENT"); } } if (getOrientation() == VERTICAL) { final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY) { throw new RuntimeException("垂直方向,高度必须固定,可以设置MATCH_PARENT"); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } private void initView(Context context, @Nullable AttributeSet attrs) { int orientation = LinearLayout.HORIZONTAL; if (attrs != null) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CardSlideView); mSideOffsetPercent = ta.getFloat(R.styleable.CardSlideView_card_side_offset_percent, 0.25f); mIsLoop = ta.getBoolean(R.styleable.CardSlideView_card_loop, mIsLoop); mItemRate = ta.getFloat(R.styleable.CardSlideView_card_item_rate, 1.3f); orientation = ta.getInt(R.styleable.CardSlideView_android_orientation, orientation); ta.recycle(); } if (mSideOffsetPercent < MIN_OFFSET_PERCENT) { mSideOffsetPercent = MIN_OFFSET_PERCENT; } if (mSideOffsetPercent > MAX_OFFSET_PERCENT) { mSideOffsetPercent = MAX_OFFSET_PERCENT; } mCardListView = new InnerRecyclerView(context); mCardListView.setHasFixedSize(true); mCardListView.setNestedScrollingEnabled(false); mCardListView.setOverScrollMode(OVER_SCROLL_NEVER); addView(mCardListView); mLayoutManager = new GalleryLayoutManager(orientation, mIsLoop); mLayoutManager.setItemTransformer(new ScaleTransformer()); mLayoutManager.attachToRecyclerView(mCardListView); } public void setSnapHelper(SnapHelper snapHelper) { mLayoutManager.setSnapHelper(snapHelper); } public void bind(List<T> data, @NonNull CardHolder<T> holder) { final int orientation = getOrientation(); if (orientation == LinearLayout.HORIZONTAL) { if (mHorizontalAdapter == null) { mHorizontalAdapter = new CardAdapter<>(data, holder, mSideOffsetPercent, orientation, mItemRate); mCardListView.setAdapter(mHorizontalAdapter); return; } mHorizontalAdapter.notifyChanged(data); return; } if (mVerticalAdapter == null) { mVerticalAdapter = new CardAdapter<>(data, holder, mSideOffsetPercent, orientation, mItemRate); mCardListView.setAdapter(mVerticalAdapter); return; } mVerticalAdapter.notifyChanged(data); } public void setOrientation(@Orientation int orientation) { mLayoutManager.setOrientation(orientation); } public int getOrientation() { return mLayoutManager.getOrientation(); } public void setCurrentItem(int item) { setCurrentItem(item, true); } public void setCurrentItem(int item, boolean smoothScroll) { if (smoothScroll) { mCardListView.smoothScrollToPosition(item); return; } mCardListView.scrollToPosition(item); } public int getCurrentItem() { return mLayoutManager.getCurrentItem(); } public void setLooper(boolean isLooper) { mLayoutManager.setLooper(isLooper); } public void setItemTransformer(PageTransformer pageTransformer) { mLayoutManager.setItemTransformer(pageTransformer); } public void setOnPageChangeListener(OnPageChangeListener onPageChangeListener) { mLayoutManager.setOnPageChangeListener(onPageChangeListener); } static class CardAdapter<T> extends RecyclerView.Adapter<CardViewHolder> { private List<T> mData; @NonNull private CardHolder<T> mHolder; private float mSideOffsetPercent; private int mOrientation; private float mItemRate; CardAdapter(List<T> data, @NonNull CardHolder<T> holder, float sideOffsetPercent, int orientation, float itemRate) { mData = data; mHolder = holder; mSideOffsetPercent = sideOffsetPercent; mOrientation = orientation; mItemRate = itemRate; } void notifyChanged(List<T> data) { mData = data; notifyDataSetChanged(); } @NonNull @Override public CardViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { final Context context = parent.getContext(); final View view = mHolder.onCreateView(LayoutInflater.from(context), parent); RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); if (mOrientation == LinearLayout.HORIZONTAL) { params.width = Math.round(parent.getMeasuredWidth() / (mSideOffsetPercent * 2 + 1)); params.height = Math.round(params.width * mItemRate); } else { params.height = Math.round(parent.getMeasuredHeight() / (mSideOffsetPercent * 2 + 1)); params.width = Math.round(params.height / mItemRate); } return new CardViewHolder(view); } @Override public void onBindViewHolder(@NonNull CardViewHolder holder, int position) { mHolder.onBindView(holder, mData.get(position), position); } @Override public int getItemCount() { return mData == null ? 0 : mData.size(); } } private class InnerRecyclerView extends RecyclerView { /** * 水平可以顺畅滑动的范围-45~45和135~225 */ public static final double RANGE_VALUE_ABS = Math.PI / 4D; private float downX; private float downY; private InnerRecyclerView(Context context) { super(context); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: downX = ev.getX(); downY = ev.getY(); break; case MotionEvent.ACTION_MOVE: float moveX = ev.getX(); float moveY = ev.getY(); double atan = Math.atan(Math.abs(moveY - downY) / Math.abs(moveX - downX)); if (atan >= 0 && atan <= RANGE_VALUE_ABS) { getParent().requestDisallowInterceptTouchEvent(true); } break; default: break; } return super.dispatchTouchEvent(ev); } } }