跳至主要內容

08.自定义组合控件-轮播图


自定义组合控件-轮播图

自定义控件系列课程在这里

Android开发自定义控件系列课程open in new window

前面我们做了一个自定义组合控件,登录的界面。

开发日常-编写一个登录界面open in new window

接下来,我们写一个轮播图的例子

20191111_221307.png
20191111_221307.png

分析

轮播图可以由哪些控件组成呢?

轮播的内容可以用ViewPager来显示吧

文字可以用TextView来显示吧

圆点可以用View来显示吧

约定

  • 一般自己写的View放在一个views的包下,所以创建一个views的目录吧
  • 命名,公司名/项目名为前缀,后面为功能名。当然啦,你也可以用你对象的名字或者孩子的名字来命名。
  • 类顶部的注释要有详细的使用方法

代码编写

其实组合控件,按步骤来就好

继承自LinearLayout或RelatviewLayout

public class SobLooperView extends LinearLayout {
    public SobLooperView(Context context) {
        //确保统一入口
        this(context,null);
    }

    public SobLooperView(Context context,@Nullable AttributeSet attrs) {
        //确保统一入口
        this(context,attrs,0);
    }

    public SobLooperView(Context context,@Nullable AttributeSet attrs,int defStyleAttr) {
        super(context,attrs,defStyleAttr);
    }
}

那我不继承自LinerLayout或者RelativeLayout可以吗?

当然可以,继承自一个ViewGroup只是作为容器来方其他要组合在一起的子控件。

编写要组合的控件内容

根据前面的分析,我们写成这个样子:layout_looper_view.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--view pager,我用的是androidx的,不是以前的v4包-->
    <androidx.viewpager.widget.ViewPager
        android:id="@+id/content_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!--标题控件-->
    <TextView
        android:id="@+id/content_title"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginBottom="20dp"
        android:text="这是标题内容..." />

    <!--用来放圆点-->
    <LinearLayout
        android:id="@+id/content_point_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="10dp"
        android:orientation="horizontal" />

</RelativeLayout>

把需要的组合的控件创建或者加入进来

把布局layout_looper_view.xml载入到我们前面写好的容器里

请详细看看注释内容,这就给大家解释了什么时候,inflate最后一个参数attach填ture还是false了。

public class SobLooperView extends LinearLayout {
    public SobLooperView(Context context) {
        //确保统一入口
        this(context,null);
    }

    public SobLooperView(Context context,@Nullable AttributeSet attrs) {
        //确保统一入口
        this(context,attrs,0);
    }

    public SobLooperView(Context context,@Nullable AttributeSet attrs,int defStyleAttr) {
        super(context,attrs,defStyleAttr);
        //ViewPager
        //TextView
        //点容器,点需要动态地创建,因为点的个数跟内容个数有关系,同学们现在明白第三个参数为什么填写true了吧。
        //填写true的话,就是自动填充到前面的viewGroup里
        LayoutInflater.from(context).inflate(R.layout.layout_looper_view,this,true);
        //等价于如下:
        //View content = LayoutInflater.from(context).inflate(R.layout.layout_looper_view,this,false);
        //addView(content);
    }
}

找到子控件

我们把view都绑到当前的view里了,所以可以用this.findViewById()

public class SobLooperView extends LinearLayout {

    private ViewPager mViewPager;
    private TextView mTitleView;
    private LinearLayout mPointCotainer;

    public SobLooperView(Context context) {
        //确保统一入口
        this(context,null);
    }

    public SobLooperView(Context context,@Nullable AttributeSet attrs) {
        //确保统一入口
        this(context,attrs,0);
    }

    public SobLooperView(Context context,@Nullable AttributeSet attrs,int defStyleAttr) {
        super(context,attrs,defStyleAttr);
        //ViewPager
        //TextView
        //点容器,点需要动态地创建,因为点的个数跟内容个数有关系,同学们现在明白第三个参数为什么填写true了吧。
        //填写true的话,就是自动填充到前面的viewGroup里
        LayoutInflater.from(context).inflate(R.layout.layout_looper_view,this,true);
        //等价于如下:
        //View content = LayoutInflater.from(context).inflate(R.layout.layout_looper_view,this,false);
        //addView(content);
        initView();
    }

    /**
     * 找到子控件
     */
    private void initView() {
        mViewPager = this.findViewById(R.id.content_pager);
        mTitleView = this.findViewById(R.id.content_title);
        mPointCotainer = this.findViewById(R.id.content_point_container);
    }
}

提供设置数据的方法和回调

我们这里面,可以让外部把数据给进来,包括图片的地址/路径,标题。但是,如果是内部去实现这些功能,那扩展性就不好了。

那你是网络地址,还是本地图片呢?所以呢,我们让外面去给一个适配器进来,设置给pager就好了,使用的人爱怎么设置就怎么设置。如果作为设计的人,你还得考虑后面的无限轮播。无限轮播我们

那还有,标题呢?我们可以定好接口,把当前的position传出去呀,至于要设置什么内容,我才不管呢。

所以就有了:

   //提供方法给外部设置适配器进来,这个适配器怎么我们有规定,所以使用了一个抽象类来描述。
    //而TitleBindListener用来获取标题,我们要标题的时候调用即可。
    public void setData(InnerPageAdapter innerPageAdapter,TitleBindListener listener) {
        mViewPager.setAdapter(innerPageAdapter);
        this.mTitleBindListener = listener;
    }

    public interface TitleBindListener {
        String getTitle(int position);
    }

    public static abstract class InnerPageAdapter extends PagerAdapter {

        public abstract int getDataSize();

        @Override
        public int getCount() {
            //因为要无限轮播嘛,所以我们就给一个IntegerMaxValue
            return Integer.MAX_VALUE;
        }

        @Override
        public boolean isViewFromObject(@NonNull View view,@NonNull Object object) {
            return view == object;
        }

        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container,int position) {
            //载入view,至于显示什么view,不用管,由外面给进来。只要对position进行一个转换
            int itemPosition = position % getDataSize();
            View itemView = getItemView(container,itemPosition);
            container.addView(itemView);
            return itemView;
        }

        //至于要什么view,我不管,由外面给我。
        protected abstract View getItemView(ViewGroup container,int itemPosition);

        @Override
        public void destroyItem(@NonNull ViewGroup container,int position,@NonNull Object object) {
            container.removeView((View) object);
        }
    }

到这里,pager的内容就有了,但是呢,标题和圆点没整呢,怎么整呢?

设置pager的滑动切换监听

20191116_145841.png
20191116_145841.png
/**
     * 设置相关事件
     */
    private void initEvent() {
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {

            /**
             *
             * @param position position有两个情况,position positionOffset 为0时,就是当前的Position
             *                 如果有滑动,position则会是下一个准备看到的position
             *
             * @param positionOffset 位置偏移量,取值为0到1,[0,1)
             * @param positionOffsetPixels 位置偏移量,这个是像素界别的
             */
            @Override
            public void onPageScrolled(int position,float positionOffset,int positionOffsetPixels) {
                //滑动时的回调
            }

            @Override
            public void onPageSelected(int position) {
                //滑动以后停下来的回调,position指所停在的位置
                //这个时候我们去获取标题
                if(mTitleBindListener != null) {
                    String title = mTitleBindListener.getTitle(position);
                    mTitleView.setText(title);
                }
            }

            @Override
            public void onPageScrollStateChanged(int state) {
                //滑动状态的改变,有停止的,滑动中的.
                //ViewPager#SCROLL_STATE_IDLE
                //ViewPager#SCROLL_STATE_DRAGGING
                //ViewPager#SCROLL_STATE_SETTLING
            }
        });
    }

这样子,当页面选中的时候,就会去调用外部设置进来的接口实现方法获得标题,设置到控件上。

动态添加圆点

为什么要动态添加呢?因为我们不知道有多少个内容呀,所以等到使用者把数据设置进来以后,我们就根据数据动态地创建圆点,添加到容器里。

所以就有了以下的代码。

private void updateIndicator() {
        if(mInnerAdapter != null) {
            //先删除
            mPointContainer.removeAllViews();
            int indicatorSize = mInnerAdapter.getDataSize();
            for(int i = 0; i < indicatorSize; i++) {
                View view = new View(getContext());
                if((mViewPager.getCurrentItem() % mInnerAdapter.getDataSize() == i)) {
                    view.setBackgroundColor(Color.parseColor("#ff0000"));
                } else {
                    view.setBackgroundColor(Color.parseColor("#ffffff"));
                }
                LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(SizeUtils.dip2px(getContext(),5),SizeUtils.dip2px(getContext(),5));
                layoutParams.setMargins(SizeUtils.dip2px(getContext(),5),0,SizeUtils.dip2px(getContext(),5),0);
                view.setLayoutParams(layoutParams);
                //添加到容器里
                mPointContainer.addView(view);
            }
        }
    }

这样子也不好表达,这样子吧,我把代码贴出来,然后去看视频好了。

自定义属性

自定义属性的话看视频吧

代码

SizeUtils.java工具类

package com.sunofbeaches.looperdemo.views;

import android.content.Context;

public class SizeUtils {

    public static int dip2px(Context context,float dpValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

}

SobViewPager.java

这个是覆写了ViewPager,做自动轮播处理

package com.sunofbeaches.looperdemo.views;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.ViewPager;

public class SobViewPager extends ViewPager {


    private Handler mHandler;

    public SobViewPager(@NonNull Context context) {
        this(context,null);
        setPageTransformer(true,new PageTransformer() {
            @Override
            public void transformPage(@NonNull View page,float position) {

            }
        });
    }

    public SobViewPager(@NonNull Context context,@Nullable AttributeSet attrs) {
        super(context,attrs);
        mHandler = new Handler(Looper.getMainLooper());
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v,MotionEvent event) {
                //不处理事件
                int action = event.getAction();
                switch(action) {
                    case MotionEvent.ACTION_DOWN:
                        pauseLooper();
                        break;
                    case MotionEvent.ACTION_CANCEL:
                    case MotionEvent.ACTION_UP:
                        resumeLooper();
                        break;
                }
                return false;
            }
        });
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        this.resumeLooper();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        this.pauseLooper();
    }

    private void resumeLooper() {
        //继续轮播
        mHandler.postDelayed(mTask,1000);
    }

    private Runnable mTask = new Runnable() {
        @Override
        public void run() {
            int currentItem = getCurrentItem();
            currentItem++;
            setCurrentItem(currentItem);
            mHandler.postDelayed(this,1000);
        }
    };

    private void pauseLooper() {
        //暂停轮播
        mHandler.removeCallbacks(mTask);
    }
}

这是组合控件的类,SobLooperView.java

package com.sunofbeaches.looperdemo.views;

import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.sunofbeaches.looperdemo.R;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;

public class SobLooperView extends LinearLayout {

    private ViewPager mViewPager;
    private TextView mTitleView;
    private LinearLayout mPointContainer;
    private TitleBindListener mTitleBindListener = null;
    private InnerPageAdapter mInnerAdapter = null;

    public SobLooperView(Context context) {
        //确保统一入口
        this(context,null);
    }

    public SobLooperView(Context context,@Nullable AttributeSet attrs) {
        //确保统一入口
        this(context,attrs,0);
    }

    public SobLooperView(Context context,@Nullable AttributeSet attrs,int defStyleAttr) {
        super(context,attrs,defStyleAttr);
        //ViewPager
        //TextView
        //点容器,点需要动态地创建,因为点的个数跟内容个数有关系,同学们现在明白第三个参数为什么填写true了吧。
        //填写true的话,就是自动填充到前面的viewGroup里
        LayoutInflater.from(context).inflate(R.layout.layout_looper_view,this,true);
        //等价于如下:
        //View content = LayoutInflater.from(context).inflate(R.layout.layout_looper_view,this,false);
        //addView(content);
        initView();
        initEvent();
    }

    /**
     * 设置相关事件
     */
    private void initEvent() {
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {

            /**
             *
             * @param position position有两个情况,position positionOffset 为0时,就是当前的Position
             *                 如果有滑动,position则会是下一个准备看到的position
             *
             * @param positionOffset 位置偏移量,取值为0到1,[0,1)
             * @param positionOffsetPixels 位置偏移量,这个是像素界别的
             */
            @Override
            public void onPageScrolled(int position,float positionOffset,int positionOffsetPixels) {
                //滑动时的回调
            }

            @Override
            public void onPageSelected(int position) {
                //滑动以后停下来的回调,position指所停在的位置
                //这个时候我们去获取标题
                if(mTitleBindListener != null && mInnerAdapter != null) {
                    String title = mTitleBindListener.getTitle(position % mInnerAdapter.getDataSize());
                    mTitleView.setText(title);
                }
                updateIndicator();
            }

            @Override
            public void onPageScrollStateChanged(int state) {
                //滑动状态的改变,有停止的,滑动中的.
                //ViewPager#SCROLL_STATE_IDLE
                //ViewPager#SCROLL_STATE_DRAGGING
                //ViewPager#SCROLL_STATE_SETTLING
            }
        });
    }

    /**
     * 找到子控件
     */
    private void initView() {
        mViewPager = this.findViewById(R.id.content_pager);
        mViewPager.setPageMargin(SizeUtils.dip2px(getContext(),20));
        mViewPager.setOffscreenPageLimit(3);
        mTitleView = this.findViewById(R.id.content_title);
        mPointContainer = this.findViewById(R.id.content_point_container);
    }


    //提供方法给外部设置适配器进来,这个适配器怎么我们有规定,所以使用了一个抽象类来描述。
    //而TitleBindListener用来获取标题,我们要标题的时候调用即可。
    public void setData(InnerPageAdapter innerPageAdapter,TitleBindListener listener) {
        mViewPager.setAdapter(innerPageAdapter);
        mViewPager.setCurrentItem(Integer.MAX_VALUE / 2 + 1);
        this.mInnerAdapter = innerPageAdapter;
        this.mTitleBindListener = listener;
        if(mTitleBindListener != null) {
            String title = mTitleBindListener.getTitle(0);
            mTitleView.setText(title);
        }
        //创建圆点
        updateIndicator();
        innerPageAdapter.registerDataSetObserver(new DataSetObserver() {
            @Override
            public void onChanged() {
                updateIndicator();
            }
        });
    }

    private void updateIndicator() {
        if(mInnerAdapter != null) {
            //先删除
            mPointContainer.removeAllViews();
            int indicatorSize = mInnerAdapter.getDataSize();
            for(int i = 0; i < indicatorSize; i++) {
                View view = new View(getContext());
                if((mViewPager.getCurrentItem() % mInnerAdapter.getDataSize() == i)) {
                    view.setBackgroundColor(Color.parseColor("#ff0000"));
                } else {
                    view.setBackgroundColor(Color.parseColor("#ffffff"));
                }
                LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(SizeUtils.dip2px(getContext(),5),SizeUtils.dip2px(getContext(),5));
                layoutParams.setMargins(SizeUtils.dip2px(getContext(),5),0,SizeUtils.dip2px(getContext(),5),0);
                view.setLayoutParams(layoutParams);
                //添加到容器里
                mPointContainer.addView(view);
            }
        }
    }

    public interface TitleBindListener {
        String getTitle(int position);
    }

    public static abstract class InnerPageAdapter extends PagerAdapter {

        public abstract int getDataSize();

        @Override
        public int getCount() {
            //因为要无限轮播嘛,所以我们就给一个IntegerMaxValue
            return Integer.MAX_VALUE;
        }

        @Override
        public boolean isViewFromObject(@NonNull View view,@NonNull Object object) {
            return view == object;
        }

        @NonNull
        @Override
        public Object instantiateItem(@NonNull ViewGroup container,int position) {
            //载入view,至于显示什么view,不用管,由外面给进来。只要对position进行一个转换
            int itemPosition = position % getDataSize();
            View itemView = getItemView(container,itemPosition);
            container.addView(itemView);
            return itemView;
        }

        //至于要什么view,我不管,由外面给我。
        protected abstract View getItemView(ViewGroup container,int itemPosition);

        @Override
        public void destroyItem(@NonNull ViewGroup container,int position,@NonNull Object object) {
            container.removeView((View) object);
        }
    }

}

相关布局:layout_looper_view.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:clipChildren="false"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--view pager,我用的是androidx的,不是以前的v4包-->
    <com.sunofbeaches.looperdemo.views.SobViewPager
        android:id="@+id/content_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false"
        android:layout_marginLeft="40dp"
        android:layout_gravity="center"
        android:layout_marginRight="40dp" />

    <!--标题控件-->
    <TextView
        android:id="@+id/content_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#99ffffff"
        android:paddingLeft="20dp"
        android:paddingTop="2dp"
        android:paddingRight="20dp"
        android:paddingBottom="2dp"
        android:text="这是标题内容..."
        android:textAlignment="center"
        android:textSize="12sp" />

    <!--用来放圆点-->
    <LinearLayout
        android:id="@+id/content_point_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="10dp"
        android:orientation="horizontal" />

</RelativeLayout>

使用写好的控件

布局上引用

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <com.sunofbeaches.looperdemo.views.SobLooperView
        android:layout_width="match_parent"
        android:id="@+id/sob_looper"
        android:layout_height="120dp" />

</RelativeLayout>

设置数据即可

package com.sunofbeaches.looperdemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import com.sunofbeaches.looperdemo.domain.LooperItem;
import com.sunofbeaches.looperdemo.views.SobLooperView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private List<LooperItem> mData;
    private SobLooperView mLooperView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initTestData();
        initView();
    }

    private void initView() {
        mLooperView = this.findViewById(R.id.sob_looper);
        mLooperView.setData(new SobLooperView.InnerPageAdapter() {
            @Override
            public int getDataSize() {
                return mData.size();
            }

            @Override
            protected View getItemView(ViewGroup container,int itemPosition) {
                ImageView imageView = new ImageView(container.getContext());
                imageView.setScaleType(ImageView.ScaleType.FIT_XY);
                //设置图片
                imageView.setImageResource(mData.get(itemPosition).getImgRsId());
                ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
                imageView.setLayoutParams(layoutParams);
                return imageView;
            }
        },new SobLooperView.TitleBindListener() {
            @Override
            public String getTitle(int position) {
                return mData.get(position).getTitle();
            }
        });
    }

    private void initTestData() {
        mData = new ArrayList<>();
        mData.add(new LooperItem("图片1的标题",R.mipmap.pic1));
        mData.add(new LooperItem("图片2的标题",R.mipmap.pic2));
        mData.add(new LooperItem("图片3的标题",R.mipmap.pic3));
        mData.add(new LooperItem("图片4的标题",R.mipmap.pic4));
    }
}

效果

viewGroup.gif
viewGroup.gif

像素转dp工具类

public class SizeUtils {

    public static int dip2px(Context context,float dpValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

}