都说自定义View是小白和中级开发者的分界线,这也看出来自定义View在Android开发过程中的重要性,所以我的博客初期会以各种各样的自定义View为主。不会讲述太多的原理,主要提供一种简单易懂的实现。
本期要实现的是可循环拉动的动画菜单。这个控件我在其他博客上看到过不少的实现方法,但是大多数要么就是代码量太大,要么就是结构太复杂,对新手来说阅读和理解起来比较困难,所以我今天花了一些时间来实现一个简单可用的循环滚动菜单控件。
先看看效果图,这种控件提供了横向和纵向的实现
接下来进行代码讲解
代码主要是继承LinearLayout来实现的ScrollerLayout,共有21个方法,当其中只有5个是重要的,其他都是些数值设置初始化相关的方法。下面先来看看自定义的变量
/**
* 公用的变量
*/
private Context myContext;//上下文变量
private Scroller mScroller;//用于操控滚动的Scroller变量
private int mTouchSlop;//用于判断触碰操作是点击还是滚动
private int itemPadding;//子元素间距
private int moveDirection;//点击移动的方向
private int mOrientation;//布局方向
private int visiableItemNum;//屏幕可见的子元素个数
private ArrayList<View> childViewList;//子元素列表
private int next;//下一个要显示的子元素索引
private int front;//上一个要显示的子元素索引
private int totalNum;//全部子元素个数
/**
* LinearLayout为横向时使用的变量
*/
private float mXDownPos;//点击位置的x坐标
private float mXMovePos;//点击并移动后的x坐标
private float mXLastMovePos;//上一次触发滑动事件的位置
private int leftBorder;//左界限
private int rightBorder;//右界限
private int width;//屏幕宽度
private int displayWidth;//显示的子元素的宽度
/**
* LinearLayout为纵向时使用的变量
*/
private int height;//屏幕高度
private float mYDownPos;//点击位置的y坐标
private float mYMovePos;//点击并移动后的y坐标
private float mYLastMovePos;//上一次触发滑动事件的位置
private int topBorder;//上界限
private int bottomBorder;//下界限
private int displayHeight;//显示的子元素的高度
private int statusBarHeight;//状态栏高度
private int titleBarHeight;//标题栏高度
private int navigationBarHeight;//底部导航栏(部分Android手机的按键高度也属于导航栏高度)
之前说过这个控件提供了横向和纵向的功能,只需要在XML布局文件中将android:orientation进行设置即可。下面的是所有与数值处理有关的方法,共16个
/**
* 构造函数
*/
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.setGravity(Gravity.CENTER_VERTICAL);
this.myContext=context;
init();
}
/**
* 初始化
*/
public void init(){
this.mScroller = new Scroller(this.myContext);//创建Scroller实例
ViewConfiguration configuration = ViewConfiguration.get(this.myContext);//获取TouchSlop值
this.mTouchSlop = configuration.getScaledTouchSlop();
}
/**
* 设置屏幕上可见的子元素个数
* @param value
*/
public void initVisiableItemNum(int value){
this.visiableItemNum=(value==0)?getChildCount():value;
initTotalNum();//获取子元素总个数
initFront();//获取上一个要显示的子元素索引
initNext();//获取下一个要显示的子元素索引
}
/**
* 获得子元素列表
*/
private void initChildViewList(){
this.childViewList=new ArrayList<View>();
int childCount = getChildCount();//获取包含的子元素的个数
for (int i = 0; i < childCount; i++) {
this.childViewList.add(getChildAt(i));
}
}
/**
* 初始化状态栏高度
*/
private void initStatusBarHeight(){
int result = 0;
int resourceId = this.myContext.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = this.myContext.getResources().getDimensionPixelSize(resourceId);
}
this.statusBarHeight=result;
}
/**
* 初始化导航栏高度
*/
private void initNavigationBarHeight(){
int result=0;
Resources resources = this.myContext.getResources();
int resourceId=resources.getIdentifier("navigation_bar_height","dimen","android");
if(resourceId!=0)
this.navigationBarHeight = resources.getDimensionPixelSize(resourceId);
}
/**
* 初始化标题栏高度
*/
private void initTitleBarHeight(){
TypedValue tv = new TypedValue();
if (this.myContext.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
this.titleBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, this.myContext.getResources().getDisplayMetrics());
}
}
/**
* 获取屏幕高和宽的函数
*/
private void initScreenWH(){
WindowManager wm1 = (WindowManager) this.myContext.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm1.getDefaultDisplay().getMetrics(dm);
this.width = dm.widthPixels;
this.height = dm.heightPixels;
initNavigationBarHeight();
initStatusBarHeight();
initTitleBarHeight();
this.height=this.height-this.statusBarHeight-this.titleBarHeight-this.navigationBarHeight;
}
/**
* 获取LinearLayout布局方向的函数
*/
private void initOrientation(){
this.mOrientation=this.getOrientation();
}
/**
* 初始化显示的子元素的宽高和padding
*/
private void initDisplayWHP(){
if(this.mOrientation==LinearLayout.HORIZONTAL) {
this.displayWidth = this.width /this.visiableItemNum;
this.itemPadding=this.displayWidth/(this.visiableItemNum-1);
setGravity(Gravity.CENTER_VERTICAL);
}else{
this.displayHeight=this.height/this.visiableItemNum;
this.itemPadding=this.displayHeight/(this.visiableItemNum-1);
setGravity(Gravity.CENTER_HORIZONTAL);
}
}
/**
* 初始化子元素总数
*/
private void initTotalNum(){
this.totalNum=getChildCount();
}
/**
* 初始化下一个显示的元素索引
*/
private void initNext(){
this.next=visiableItemNum+1;
}
/**
* 初始化上一个显示的元素索引
*/
private void initFront(){
this.front=this.totalNum;
}
/**
* 得到下一个显示的元素索引
* @return
*/
private int getNext(){
if(this.next>this.totalNum);
this.next=1;
return this.next++;
}
/**
* 得到上一个要显示的元素索引
* @return
*/
private int getFront(){
if(this.front<1){
this.front=this.totalNum;
}
return this.front--;
}
接着就是自定义这个ViewGroup时比较重要的函数了,首先是onmeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();//获取包含的子元素的个数
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
initScreenWH();//初始化屏幕宽度和高度
initOrientation();//初始化屏幕方向
initDisplayWHP();//初始化展示的子元素的宽度和高度以及padding
}
然后是处理动画效果的startAnimation函数
/**
* 启动每个子元素形状变化的动画函数
*/
private void startAnimate(){//根据显示的子元素的个数对透明度和XY缩小的比例进行调整
float temp0=(this.visiableItemNum-1)/(float)2+1;
float temp1=1/(float)temp0;
float rate;
for(int i=0;i<this.visiableItemNum;i++){
rate=(float) ((i+1)*temp1);
if(rate>1){
rate=2-rate;
}
getChildAt(i).animate().alpha(rate).scaleY(rate).scaleY(rate);
}
}
接着是处理布局变化的onLayout函数
/**
* 布局函数
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < this.totalNum; i++) {
View childView = this.childViewList.get(i);
if(this.mOrientation==LinearLayout.HORIZONTAL) {//当布局为横向时
childView.layout(i * (this.displayWidth + this.itemPadding) - this.displayWidth / 2, 0, (i + 1) * (this.displayWidth) + i * this.itemPadding - this.displayWidth / 2, childView.getMeasuredHeight());
this.leftBorder = getChildAt(0).getLeft();
this.rightBorder = getChildAt(this.visiableItemNum-1).getRight();
}
else{//当布局为纵向时
childView.layout(0,i * (this.displayHeight + this.itemPadding) - this.displayHeight / 2+statusBarHeight, childView.getMeasuredWidth(), (i + 1) * (this.displayHeight) + i * this.itemPadding - this.displayHeight / 2+statusBarHeight);
this.topBorder = getChildAt(0).getTop();
this.bottomBorder = getChildAt(this.visiableItemNum-1).getBottom();
}
}
startAnimate();//每次布局结束后都开启动画
}
接着就是处理动作交互的函数了,首先用onintercepttouchevent方法判断是用户的动作点击还是滑动
/**
* 用于判断用户的Touch属于点击还是滚动的函数
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN://点击时获得x,y坐标
mXDownPos = ev.getRawX();
mYDownPos=ev.getRawY();
mXLastMovePos = mXDownPos;
mYLastMovePos=mYMovePos;
break;
case MotionEvent.ACTION_MOVE://判断手指移动的距离有没有达到定性为滑动的最小距离
float distance;
if(this.mOrientation==LinearLayout.HORIZONTAL) {
mXMovePos = ev.getRawX();
distance= Math.abs(mXMovePos - mXDownPos);
mXLastMovePos = mXMovePos;
}else{
mYMovePos = ev.getRawY();
distance = Math.abs(mYMovePos - mYDownPos);
mYLastMovePos = mYMovePos;
}
if (distance > mTouchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
如果判断为滑动,就交给onTouchEvent处理
/**
* 响应触碰事件的函数,我们在这里进行滚动操作的处理
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
if(this.mOrientation==LinearLayout.HORIZONTAL) {//当为横向布局时
mXMovePos = event.getRawX();
int scrolledX = (int) (mXLastMovePos - mXMovePos);
if (getScrollX() + scrolledX < leftBorder) {//限制活动范围不能超过界限
scrollTo(leftBorder, 0);
return true;
} else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
scrollTo(rightBorder - getWidth(), 0);
return true;
}
moveDirection = scrolledX;
scrollBy(scrolledX, 0);
mXLastMovePos = mXMovePos;
}else{//当为纵向布局时
mYMovePos = event.getRawY();
int scrolledY = (int) (mYLastMovePos - mYMovePos);
if (getScrollY() + scrolledY < topBorder) {
scrollTo(0, topBorder);
return true;
} else if (getScrollY() + getHeight() + scrolledY > this.bottomBorder) {
scrollTo(0, this.bottomBorder-this.height);
return true;
}
moveDirection = scrolledY;
scrollBy(0, scrolledY);
mYLastMovePos = mYMovePos;
}
break;
case MotionEvent.ACTION_UP://当手指释放后
int targetIndex;
if(this.mOrientation==LinearLayout.HORIZONTAL) {
targetIndex = (getScrollX() + displayWidth / 2) / (displayWidth + itemPadding);
int dx = getScrollX() - targetIndex * (displayWidth + itemPadding);
mScroller.startScroll(getScrollX(), 0, -dx, 0);//调用startScroll实现滚动刷新
}else{
targetIndex = (getScrollY() + displayHeight / 2) / (displayHeight + itemPadding);
int dx = getScrollY() - targetIndex * (displayHeight + itemPadding);
mScroller.startScroll(0, getScrollY(), 0, -dx);//调用startScroll实现滚动刷新
}
if(moveDirection<0) {//这个是实现循环的关键,根据滑动的方向添加移除View
int temp=getFront();
removeView(this.childViewList.get(temp-1));
addView(this.childViewList.get(temp-1),0);
requestLayout();//请求重写布局
}
else if(moveDirection>0) {
int temp=getNext();
removeView(this.childViewList.get(temp-1));
addView(this.childViewList.get(temp-1),this.totalNum-1);
requestLayout();
}
invalidate();//刷新
break;
}
return super.onTouchEvent(event);
}
接着是与Scroller的实现有关的computeScroll函数
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
这就是这个控件基本的代码,注意,这里有两个个小小的bug,就是当布局为纵向时而屏幕为横向时会出现无法滚动的现象,如下
这里可能是由于我在纵向位置的处理上出现了问题,而且可见的子元素个数为了美观可用必须为大于1的奇数。之后等我手头上的几个项目完工后再对这个控件进行改进
下面我们看看如何使用这个控件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.drw.scroller.MainActivity">
<com.drw.scroller.ScrollerLayout//这里用到了自定义控件
android:id="@+id/myScrollerLayout"
android:layout_alignParentLeft="true"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="match_parent">
<ImageView//内置的子元素
android:clickable="true"
android:layout_width="100dp"//这里将子元素的长和宽都设置为100dp是应为懒得处理图像,实际上ScrollerLayout中不会理会这个设置,之后会和纵向问题一起改进
android:layout_height="100dp"
android:src="@mipmap/btn1"/>
<ImageView
android:clickable="true"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/btn2"/>
<ImageView
android:clickable="true"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/btn3"/>
<ImageView
android:clickable="true"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/btn4"/>
<ImageView
android:clickable="true"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/btn5"/>
</com.drw.scroller.ScrollerLayout>
</RelativeLayout>
接着看看主类
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ScrollerLayout ms=(ScrollerLayout)findViewById(R.id.myScrollerLayout);
ms.initVisiableItemNum(3);//只需要设置一下可见的子元素个数即可,注意这里的个数要大于1,且为了美观最好为奇数,额,这也算一个Bug吧
}
}
好了,这个控件的展示就到这里了,虽然有点小bug,但是之后等我的项目做完后会对这个控件进行改进。当然,大家有什么好的改进方法也可以留言与我交流。让我也学习学习
啊,还有项目的源码链接:http://download.youkuaiyun.com/download/qq_37656219/10254838
我是菜鸟,多多指教,DRW
————————————————————————
我又对代码进行了一些修改,发现了在纵向布局时的解决方法。把initScreenWH中的initNavigationBarHeight函数注释掉
,然后在源代码中把这个函数和有关的this.navigationBarHeight去掉,这个函数和变量就是影响横纵向的关键所在
/**
* 获取屏幕高和宽的函数
*/
private void initScreenWH(){
WindowManager wm1 = (WindowManager) this.myContext.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics dm = new DisplayMetrics();
wm1.getDefaultDisplay().getMetrics(dm);
this.width = dm.widthPixels;
this.height = dm.heightPixels;
//initNavigationBarHeight();
initStatusBarHeight();
initTitleBarHeight();
this.height=this.height-this.statusBarHeight-this.titleBarHeight;
}
接着再对initDisplayWH,onLayout,构造函数代码进行修改,添加layoutTop和layoutLeft变量,以及自定义属性layoutLocation
private int layoutTop;//控件最上面举例屏幕顶端高度
private int layoutLeft;//控件最左面距离屏幕左边宽度
private void initDisplayWHP(){
this.displayWidth = this.width /this.visiableItemNum;
this.itemPadding=this.displayWidth/(this.visiableItemNum-1);
this.displayHeight=this.height/this.visiableItemNum;
this.itemPadding=this.displayHeight/(this.visiableItemNum-1);
if(this.mOrientation==LinearLayout.HORIZONTAL) {
setGravity(Gravity.CENTER_VERTICAL);
}else{
setGravity(Gravity.CENTER_HORIZONTAL);
}
}
自定义的attrs.xml内容如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ScrollerLayout">
<attr name="layoutLocation" format="string" />
</declare-styleable>
</resources>
还有构造函数函数
/**
* 构造函数
*/
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this.setGravity(Gravity.CENTER_VERTICAL);
this.myContext=context;
TypedArray ta = this.myContext.obtainStyledAttributes(attrs, R.styleable.ScrollerLayout);
layoutLocation = ta.getString(R.styleable.ScrollerLayout_layoutLocation);
ta.recycle();
init();
}
/**
* 处理控件位置的函数
*/
private void initLayoutLocation(){
if(this.layoutLocation.toString().compareTo("Left")==0){
this.layoutLeft=0;
}else if(this.layoutLocation.toString().compareTo("Right")==0){
Log.e("info","lala"+this.width+","+this.displayWidth);
this.layoutLeft=this.width-displayWidth;
}else if(this.layoutLocation.toString().compareTo("Top")==0){
this.layoutTop=0;
}else{
this.layoutTop=this.height-this.displayHeight;
}
}
注意这里的字符串比较不能用==,否则永远是false接下来是onLayut函数
/**
* 布局函数
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
initLayoutLocation();//在这里调用设置位置
for (int i = 0; i < this.totalNum; i++) {
View childView = this.childViewList.get(i);
if(this.mOrientation==LinearLayout.HORIZONTAL) {
childView.layout(i * (this.displayWidth + this.itemPadding) - this.displayWidth / 2, this.layoutTop, (i + 1) * (this.displayWidth) + i * this.itemPadding - this.displayWidth / 2, this.layoutTop+childView.getMeasuredHeight());
this.leftBorder = getChildAt(0).getLeft();
this.rightBorder = getChildAt(this.visiableItemNum-1).getRight();
}
else{
childView.layout(this.layoutLeft,i * (this.displayHeight + this.itemPadding) - this.displayHeight / 2, this.layoutLeft+childView.getMeasuredWidth(), (i + 1) * (this.displayHeight) + i * this.itemPadding - this.displayHeight / 2);
this.topBorder = getChildAt(0).getTop();
this.bottomBorder = getChildAt(this.visiableItemNum-1).getBottom();
}
}
startAnimate();
}
到这里就把纵向无法拉动的问题解决了