自定义View - MyViewPager和MyTabLayout

首先声明一点,写这两个View纯粹为了练习而已,要实现这个功能完全可以直接用ViewPager和TabLayout完成
下面首先给出效果图

自定义View - MyViewPager和MyTabLayout

Activity、Adapter代码如下
public class MyViewPagerActivity extends Activity {

    private MyViewPager mVp;
    private MyTabLayout mTab;
    public static final String[] mTitles = {"视频","图片","小说","杂志","明星","体育","游戏","竞技","中医"};

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_viewpager);
        mVp = findViewById(R.id.vp);
        mTab = findViewById(R.id.tab);
        BaseAdapter adapter = new MyBaseAdapter();
        mVp.setAdapter(adapter);
        mTab.setViewPager(mVp);
    }
    class MyBaseAdapter extends BaseAdapter {

        @Override
        public int getItemCount() {
            return mTitles.length;
        }

        @Override
        public View getView(int position) {
            return MyViewManager.getInstance().getView(getApplicationContext(), position);
        }

        @Override
        public String getTitle(int position) {
            return mTitles[position];
        }
    }

    static class MyViewManager {

        private SparseArray<View> mViews = new SparseArray<>();

        public static MyViewManager getInstance() {
            return MyViewManagerHolder.mViewManager;
        }

        public View getView(Context context, int position) {
            if (mViews.get(position) != null) {
                return mViews.get(position);
            }
            RecyclerView rv = new RecyclerView(context);
            rv.setLayoutManager(new LinearLayoutManager(context));
            rv.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
            rv.setAdapter(new CommonAdapter(context, mTitles[position]));
            mViews.put(position, rv);
            return rv;
        }

        private static class MyViewManagerHolder {
            private static MyViewManager mViewManager = new MyViewManager();
        }
    }
}

public class CommonAdapter extends RecyclerView.Adapter<CommonAdapter.ViewHolder> {

    private List<String> mList;
    private OnItemClickListener mListener;
    private Context mContext;

    CommonAdapter(Context context, String title) {
        mList = new ArrayList<>();
        mContext = context;
        for (int i=0; i<100; i++) {
            mList.add(title + " " + i);
        }
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_main_layout, parent, false));
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.mTv.setText(mList.get(position));
        holder.mTv.setOnClickListener((v) -> {
            Toast.makeText(mContext, mList.get(position), Toast.LENGTH_SHORT).show();
        });
        holder.itemView.setOnClickListener((view) -> {
            if (mListener != null) {
                mListener.onItemClick(view, holder.getLayoutPosition());
            }
        });
        holder.itemView.setOnLongClickListener((view) -> {
            if (mListener != null) {
                mListener.onItemLongClick(view, holder.getLayoutPosition());
            }
            return true;
        });
    }

    @Override
    public int getItemCount() {
        return mList.size();
    }

    public void removeData(int position) {
        mList.remove(position);
        notifyItemRemoved(position);
    }

    public void setOnItemClickListener(OnItemClickListener listener) {
        mListener = listener;
    }

    class ViewHolder extends RecyclerView.ViewHolder {

        private final TextView mTv;

        private ViewHolder(@NonNull View itemView) {
            super(itemView);
            mTv = itemView.findViewById(R.id.tv);
        }
    }

    public interface OnItemClickListener {
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }
}

最后给出两个自定义View的代码
// 快速滑动也要被认为页面切换
public class MyViewPager extends ViewGroup {

    // 每个Item所需要的宽度
    private int mPerItemWidth;
    private Scroller mScroller;
    private BaseAdapter mAdapter;
    private OnPageChangeListener mPageChangeListener;
    // 当前选中的位置
    private int mCurrentPosition;
    private int mTouchDownX;
    private int mTouchDownY;
    private VelocityTracker mTracker;

    public MyViewPager(Context context) {
        this(context, null);
    }

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
        mTracker = VelocityTracker.obtain();
    }

    /**
     * 设置当前显示的界面
     * @param index 位置 0-BaseAdapter.getItemCount() - 1
     */
    public void setCurrentItem(int index) {
        int endTranslationX = -index * mPerItemWidth;
        mScroller.startScroll((int)getTranslationX(), 0, (int)(endTranslationX - getTranslationX()), 0, 1000);
        mCurrentPosition = index;
        invalidate();
    }

    /**
     * 设置adapter用于将布局生效
     * @param adapter baseAdapter
     */
    public void setAdapter(BaseAdapter adapter) {
        if (adapter != null && adapter.getItemCount() > 0) {
            removeAllViews();
            for(int i=0; i<adapter.getItemCount(); i++) {
                addView(adapter.getView(i), new ViewGroup.LayoutParams(-1, -1));
            }
            mAdapter = adapter;
        }
    }

    /**
     * 设计监听当页面切换或者滑动
     * @param listener 监听器
     */
    public void setOnPageChangeListener(OnPageChangeListener listener) {
        mPageChangeListener = listener;
    }

    /**
     * 获取adapter
     * @return 返回当前ViewPager的Adapter
     */
    public BaseAdapter getAdapter() {
        return mAdapter;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            setTranslationX(mScroller.getCurrX());
            invalidate();
            if (mPageChangeListener != null) {
                mPageChangeListener.onTranslationXChange(getTranslationX() / mPerItemWidth);
            }
        }
    }

    // 宽度固定为childCount * 父View的给的高度
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (hasChildView()) {
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec) * mAdapter.getItemCount(),
                    MeasureSpec.getSize(heightMeasureSpec));
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    // 水平放置
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (hasChildView()) {
            // 每个View的宽度都是getMeasureWidth, 高度都是getMeasureHeight
            mPerItemWidth = getMeasuredWidth() / getChildCount();
            for (int i=0; i<getChildCount(); i++) {
                View childView = getChildAt(i);
                childView.layout( mPerItemWidth * i, 0,
                        mPerItemWidth * (i + 1), getMeasuredHeight());
            }
        }
        Log.d("onLayoutParent", " left: " + l + " top: " + t + " right: " + r + " bottom: " + b);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int currentX = (int) event.getX();
        int currentY = (int) event.getY();
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mTracker.addMovement(event);
                mTouchDownX = currentX; // 这里获取到按下的事件
                mTouchDownY = currentY;
                break;
            case MotionEvent.ACTION_MOVE:
                mTracker.addMovement(event);
                int dx = currentX - mTouchDownX;
                int dy = currentY - mTouchDownY;
                if (Math.sqrt(dx * dx + dy * dy) > ViewConfiguration.get(getContext()).getScaledTouchSlop() &&
                        Math.abs(dx) > Math.abs(dy)) {
                    return true;
                }
                break;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }
        int currentX = (int) event.getX();
        switch(event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int dx = currentX - mTouchDownX;
                setTranslationX(getTranslationX() + dx);
                if (getTranslationX() > 0) { // 左边被全部拉出去了
                    setTranslationX(0);
                    mTouchDownX = currentX;
                } else if (getTranslationX() < -mPerItemWidth * (getChildCount() - 1)) { // 右边被全部拉出去了
                    setTranslationX(-mPerItemWidth * (getChildCount() - 1));
                    mTouchDownX = currentX;
                }
                if (mPageChangeListener != null) {
                    mPageChangeListener.onTranslationXChange(getTranslationX() / mPerItemWidth);
                }
                break;
            case MotionEvent.ACTION_UP:
                // 判断一下滑动距离到不到一半
                int lastPosition = mCurrentPosition;
                // 有空这个地方再考虑考虑
                if (-getTranslationX() / mPerItemWidth % 1 > 0.5) { // 小于0.5代表的是滑动超过一半,代表左边屏幕中只占很小一部分
                    // 再分两种 1 -> 2 ++
                    if (-mCurrentPosition * mPerItemWidth > getTranslationX()) {
                        mCurrentPosition++;
                    }
                } else {
                    if (-mCurrentPosition * mPerItemWidth < getTranslationX()) {
                        mCurrentPosition--;
                    }
                }
                // 代表没有被滑动那么判断快速滑动
                if (lastPosition == mCurrentPosition) {
                    mTracker.computeCurrentVelocity(1000);
                    float xVelocity = mTracker.getXVelocity();
                    if (Math.abs(xVelocity) > 3000) {
                        if (xVelocity > 0) { // x变大
                            if (mCurrentPosition > 0) {
                                mCurrentPosition--;
                            }
                        } else { // x变小
                            if (mCurrentPosition < mAdapter.getItemCount() - 1) {
                                mCurrentPosition++;
                            }
                        }
                    }
                }
                setCurrentItem(mCurrentPosition);
                if (mPageChangeListener != null) {
                    mPageChangeListener.onPageChange(lastPosition, mCurrentPosition);
                }
                break;
        }
        return true;
    }


    private boolean hasChildView() {
        return mAdapter != null && mAdapter.getItemCount() > 0;
    }

    public interface OnPageChangeListener {

        void onPageChange(int oldPosition, int newPosition);
        void onTranslationXChange(float percent);
    }
}

public class MyTabLayout extends ViewGroup {

    private int mPerTabWidth = 150;
    private int mDividerHeight = 10;
    private int mDividerColor = Color.BLUE;
    private int mVisibleWidth;
    private MyViewPager mVp;
    private int mDownX;
    private int minTranslationX;
    private int mNormalTextColor = Color.BLACK;
    private int mSelectedTextColor = Color.BLUE;
    private int mSelectedPosition;
    private int mDownY;
    private static final String TAG = "MyTabLayout";

    @RequiresApi(api = Build.VERSION_CODES.M)
    public MyTabLayout(Context context) {
        this(context, null);
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    public MyTabLayout(Context context, AttributeSet set) {
        super(context, set);
        setForeground(new ColorDrawable(Color.parseColor("#22000000")));
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int childHeightMode = heightMode;
        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = (int) (getContext().getResources().getDisplayMetrics().density * 44); // 选用44dp
            childHeightMode = MeasureSpec.EXACTLY;
        }
        mVisibleWidth = widthSize;
        measureChildren(MeasureSpec.makeMeasureSpec(mPerTabWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, childHeightMode));
        setMeasuredDimension((getChildCount() - 1) * mPerTabWidth, heightSize);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i=0; i<getChildCount() - 1; i++) {
            getChildAt(i).layout(mPerTabWidth * i, 0, mPerTabWidth * (i + 1), b);
        }
        getChildAt(getChildCount() - 1).layout(0, b - mDividerHeight / 2, mPerTabWidth, b);
        minTranslationX = -getWidth() + mVisibleWidth;
    }

    public void setViewPager(MyViewPager viewPager) {
        if (viewPager != null) {
            mVp = viewPager;
            for (int i=0; i<mVp.getChildCount(); i++) {
                addTab(new Tab(mVp.getAdapter().getTitle(i)));
            }
            mVp.setOnPageChangeListener(new MyViewPager.OnPageChangeListener() {
                @Override
                public void onPageChange(int oldPosition, int newPosition) {
                    changeTextColor(oldPosition, newPosition);
                    mSelectedPosition = newPosition;
                }
                @Override
                public void onTranslationXChange(float percent) {
                    // 除了边界需要始终使Item保持在中间,怎么判断。
                    int middleItemPosition = mVisibleWidth / mPerTabWidth / 2; //可见的Item数量
                    if (-percent < middleItemPosition) {
                        Log.d(TAG, "onTranslationXChange: X" + percent);
                    } else if (-percent > Tab.count - middleItemPosition) {
                        Log.d(TAG, "onTranslationXChange: Z" + percent);
                    } else {
                        Log.d(TAG, "onTranslationXChange: Y");
                        setTranslationX((percent + middleItemPosition) * mPerTabWidth);
                    }
                    if (getTranslationX() < minTranslationX) {
                        Log.d(TAG, "onTranslationXChange: " + getTranslationX() + " " + minTranslationX);
                        setTranslationX(minTranslationX);
                    }
                    // 下面的分割线一直要动
                    getChildAt(getChildCount() - 1).setTranslationX(-mPerTabWidth * percent); // 与Tab移动相反
                }
            });
            addView(new DividerView(getContext()));
        }
    }

    public void addTab(Tab tab) {
        // 将其转化为View,然后添加到自身中去
        addView(convertTabToView(tab));
    }

    private void setCurrentItem(int position) {
        // 自身需要滑动吗。。 需要自己改变,VP动画开始并不会回调onPageChange
        mVp.setCurrentItem(position);
        changeTextColor(mSelectedPosition, position);
        mSelectedPosition = position;
    }

    private void changeTextColor(int oldPosition, int newPosition) {
        ((TextView)getChildAt(oldPosition)).setTextColor(mNormalTextColor);
        ((TextView)getChildAt(newPosition)).setTextColor(mSelectedTextColor);
    }

    private View convertTabToView(Tab tab) {
        TextView textView = new TextView(getContext());
        textView.setTextColor(mSelectedPosition == tab.id ? mSelectedTextColor : mNormalTextColor);
        textView.setText(tab.mTitleName);
        textView.setGravity(Gravity.CENTER);
        textView.setLayoutParams(new ViewGroup.LayoutParams(mPerTabWidth, -1));
        textView.setOnClickListener((view) -> {
            setCurrentItem(tab.id);
        });
        return textView;
    }

    public static class Tab {

        public String mTitleName;
        public int id = count++;
        public static int count;

        public Tab(String titleName) {
            mTitleName = titleName;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int currentX = (int) event.getX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int dx = currentX - mDownX;
                setTranslationX(getTranslationX() + dx);
                if (getTranslationX() > 0) {
                    setTranslationX(0);
                    mDownX = currentX;
                } else if (getTranslationX() < minTranslationX) {
                    setTranslationX(minTranslationX);
                    mDownX = currentX;
                }
                break;
        }
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int currentX = (int) event.getX();
        int currentY = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = currentX; // 给onTouchEvent使用
                mDownY = currentY;
                return false;
            case MotionEvent.ACTION_MOVE:
                return Math.sqrt((currentX - mDownX) * (currentX - mDownX) + (currentY - mDownY) * (currentY - mDownY)) >=
                        ViewConfiguration.get(getContext()).getScaledTouchSlop();
            case MotionEvent.ACTION_UP:
                return false;
            default:
                return false;
        }
    }

    class DividerView extends View {

        private Paint mPaint;

        public DividerView(Context context) {
            super(context);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setStrokeWidth(mDividerHeight);
            mPaint.setColor(mDividerColor);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(mPerTabWidth, mDividerHeight);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawLine(0, 0, getWidth(), 0, mPaint);
        }
    }
}