08.自定义组合控件-轮播图
自定义组合控件-轮播图
自定义控件系列课程在这里
前面我们做了一个自定义组合控件,登录的界面。
接下来,我们写一个轮播图的例子
分析
轮播图可以由哪些控件组成呢?
轮播的内容可以用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的滑动切换监听
/**
* 设置相关事件
*/
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));
}
}
效果
像素转dp工具类
public class SizeUtils {
public static int dip2px(Context context,float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
}