之前项目需要做这样一个效果,如下图片,后来又说要改回苹果那种开关按钮形式,但是我依然想做出这的效果出来,毕竟是我想出来的创意~~
效果就是拖拽中间的那个 小高达 头像时,就显示大高达头像,如果拖拽 小高达头像到大高达头像中,就逐渐从隐藏到显示一张图片,显示完以后就将小高达头像放回原位,如果小高达头像没有没有拖拽到大高达头像中,就是放回原位。
整体ui设计确实有点丑,毕竟我不是美工的,也懒得去美工,主要是要看效果嘛~~
下文,我将用 小高达头像 代替拖动的图片,大高达头像 代替固定屏幕中间图片。我觉得这样说会形象点= =、、、
准备
做出这个效果,我认为首先考虑的是ViewGroup,因为我们需要 大高达头像 固定在屏幕的中间,我们拖拽小高达头像时,是全屏的都可以拖拽的,所以宽高肯定是跟屏幕宽高一直才能使其在屏幕中自由滑动,可能会说,View也可以设置全屏啊,但是我们的小高达头像放在屏幕下方的,且一直显示在屏幕的正下方,这就意味着屏幕下方有控件是被挡住的,这就尴尬了= =。。所以选择ViewGroup来做我相信是最正确的选择~使用ViewGroup我们可以帮用户布局好,减少用户的工作量,而且不影响该效果的正常执行~~
public class DropSwitchView extends ViewGroup {
public DropSwitchView(Context context) {
this(context, null);
}
public DropSwitchView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DropSwitchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
测量
我们按照View的绘制流程形式,阐述一下,先看看测量
/**
* 平均的排列方式
*/
private final static int AVG_ARRAYMODE = 1;
/**
* 自由的排列方式
*/
private final static int FREE_ARRAYMODE = 2;
/**
* 当前childView的排列方式
*/
private int mArrayMode = AVG_ARRAYMODE;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
switch (mArrayMode) {
case AVG_ARRAYMODE:
int canUseWidth = (int) (mScreenWidth - mDropRectF.width() - (mAvgModeChildMargin * (getChildCount() - 2)) - (mAvgModeBorderMargin * 2) - (mAvgModeDBMargin * 2));
int childWidth = canUseWidth / getChildCount() + 2;
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.AT_MOST));
}
break;
case FREE_ARRAYMODE:
measureChildren(widthMeasureSpec, heightMeasureSpec);
break;
}
// Log.i(TAG, "onMeasure: mScreenHeight==>" + mScreenHeight + ",getWidth()==>" + getWidth() + ",getMeasureHeight()==>" + getMeasuredHeight() + ",statusBarHeight()==>" + statusBarHeight);
// setMeasuredDimension(mScreenWidth, mScreenHeight);
setMeasuredDimension(mScreenWidth, mScreenHeight - statusBarHeight);
}
我提供两种排列形式,onMeasure()根据这两种排列形式去测量
一种是AVG_ARRAYMODE(平均排列),即是根据控件宽度减去小高达头像的宽度还有一些间距所剩下来的宽度,平均分配给每个子View,int childWidth = canUseWidth / getChildCount() + 2,就是计算出平均的宽度, +2是因为有防止计算时候的误差值,保证全部子View加起来的宽度一定能填充剩下来的宽度。
另一种是FREE_ARRAYMODE(自由排列),即是子View自己去安排自己的宽度的测量,子View能够在这个剩下的宽度里自由的安排自己的尺寸。
之所以要提供这两种排列模式是因为看到很多的app的主页基本上都是采用这两种形式进行布局,我就兼容一下,让这个控件具有更好的兼容性,和方便~
最后,调用setMeasuredDimension方法,强制让控件填充整个屏幕,以保证效果的正常执行,这里的statusBarHeight就是状态栏的高度,减去状态栏的高度是为了让局限小高达图片能够拖拽的活动面积只有屏幕的宽度*屏幕高度-状态栏高度,毕竟 小高达头像 能够拖拽超过屏幕宽高的用户体验不太好嘛~
获取状态栏高度:
//获取手机状态栏的高度
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
布局
@Override
protected void onLayout(boolean changed,
int l, int t, int r, int b) {
int helfChildCount = getChildCount() / 2;
int childViewLeft = l;
mMaxMarginTop = 0;
mMaxMarginBotton = 0;
switch (mArrayMode) {
case AVG_ARRAYMODE:
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
if (i < helfChildCount) {
childView.layout(i == 0 ? childViewLeft += mAvgModeBorderMargin : childViewLeft, b - mBottomBGHeight - lp.topMargin, childViewLeft + childView.getMeasuredWidth(), b - lp.bottomMargin);
float distance = i == helfChildCount - 1 ? mAvgModeDBMargin - rightMarginBei : mAvgModeChildMargin - rightMarginBei;
childViewLeft += childView.getMeasuredWidth() + distance;
} else if (i == helfChildCount) {
childViewLeft += mDropRectF.width() + mAvgModeDBMargin - rightMarginBei;
childView.layout(childViewLeft, b - mBottomBGHeight - lp.topMargin, childViewLeft + childView.getMeasuredWidth(), b - lp.bottomMargin);
childViewLeft += childView.getMeasuredWidth() + mAvgModeChildMargin - rightMarginBei;
} else {
childView.layout(childViewLeft, b - mBottomBGHeight - lp.topMargin, childViewLeft + childView.getMeasuredWidth(), b - lp.bottomMargin);
childViewLeft += childView.getMeasuredWidth() + mAvgModeChildMargin - rightMarginBei;
}
if (mMaxMarginBotton < lp.bottomMargin) {
mMaxMarginBotton = lp.bottomMargin;
}
if (mMaxMarginTop < lp.topMargin) {
mMaxMarginTop = lp.topMargin;
}
}
break;
case FREE_ARRAYMODE:
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
if (i < helfChildCount) {
childViewLeft += lp.leftMargin;
childView.layout(childViewLeft, b - mBottomBGHeight - lp.topMargin, childViewLeft + childView.getMeasuredWidth(), b - lp.bottomMargin);
childViewLeft += childView.getMeasuredWidth() + lp.rightMargin;
} else if (i == helfChildCount) {
childViewLeft += mDropRectF.width();
childView.layout(childViewLeft, b - mBottomBGHeight - lp.topMargin, childViewLeft + childView.getMeasuredWidth(), b - lp.bottomMargin);
childViewLeft += childView.getMeasuredWidth() + lp.rightMargin;
} else {
childViewLeft += lp.leftMargin;
childView.layout(childViewLeft, b - mBottomBGHeight - lp.topMargin, childViewLeft + childView.getMeasuredWidth(), b - lp.bottomMargin);
childViewLeft += childView.getMeasuredWidth() + lp.rightMargin;
}
if (mMaxMarginBotton < lp.bottomMargin) {
mMaxMarginBotton = lp.bottomMargin;
}
if (mMaxMarginTop < lp.topMargin) {
mMaxMarginTop = lp.topMargin;
}
}
break;
}
mBottomBackGround.setBounds(getLeft(), getHeight() - mBottomBGHeight - mMaxMarginBotton - mMaxMarginTop, getRight(), getBottom());
}
我们还是根据两种排列模式对子View进行布局,mMaxMarginTop,和mMaxMarginBottom是存放子View中最大的MarginTop和MarginBottom,存放这两个值的用意就是为了让背景(ps:这里的背景不是指这个控件的背景,而是刚好覆盖全部子View的背景)囊括所有的子View,不然不好看啊~
mBottomBGHeight 就是背景的高度,默认是150像素(ps:应该差不多了吧= =)。
再来说说这两个排列方式的布局:
两者都是在子View的总数量里,取中间数,小高达头像在其中间,子View在从左往右的顺序地布局到小高达头像的周围。
两者的唯一的区别是:
AVG_ARRAYMODE的排列间距是分mAvgModeChildMargin(子View之间的间距),mAvgModeDBMargin(拖拽图片与子View的间距),和mAvgModeBorderMargin(左右边界的间距),如下图:
在这里,来说明一下一个地方===>rightMarginBei,是指右边接的间距根据子View的数量计算出来的平分值,即:
rightMarginBei = mAvgModeBorderMargin / (getChildCount() - 2 + 2);
为什么计算出这个值呢?因为View的layout方法直接影响了View的显示大小,如view.layout(left,top,right+50,bottom),这里的不是指view右边加50间距,而是指view右坐标+50所得出的新坐标,而view的绘制横坐标方向是从左坐标到右坐标,所以right+50并不会使其有了右间距,而是会让View显示大小向右拉伸50像素。基于这样的原因,我将其右间距切成rightMarginBei,在各个间距中设置减去这个rightMarginBei的长度,从而使得他能看起来像设置了右间距一样。所以用这个模式设置间距是会和实际的间距不一样。
FREE_ARRAYMODE 的排列间距是根据子View的Margin设置,相对于AVG_ARRAYMODE而已,自由很多,但是我觉得这个模式应该很少人用吧,又不方便,有要做很多屏幕适配,当时我想这个View能更兼容点,还是做了吧= =。。。
绘画
首先是要画背景:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//背景
mBottomBackGroundDrawable.draw(canvas);
}
mBottomBackGroundDrawable是Drawable,直接调用draw方法将canvas传进去就可以了进行绘画了,但是如何设置mBottomBackGroundDrawable的绘画范围了呢??,
就是这个了:
mBottomBackGroundDrawable.setBounds(getLeft(), getHeight() - mBottomBGHeight - mMaxMarginBotton - mMaxMarginTop, getRight(), getBottom());
那绘画整个效果的在哪里呢?请看~~
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
switch (mCurrentState) {
case STATE_SHOWFIX:
canvas.drawBitmap(mFixBitmap, mFixRectF.left, mFixRectF.top, mPaint);
canvas.drawBitmap(mDropBitmap, mDropRectF.left, mDropRectF.top, mPaint);
break;
case STATE_PLAYANIM:
mPaint.setAlpha(255 - 255 / mPlayAnimThread.times);
canvas.drawBitmap(mResultBitmap, mResultRectF.left, mResultRectF.top, mPaint);
break;
case STATE_HIDEFIX:
canvas.drawBitmap(mDropBitmap, mDropRectF.left, mDropRectF.top, mPaint);
break;
}
}
为啥要放在dispathDraw方法中编写绘画呢?因为如果在onDraw中绘画的效果,会使得其绘画的效果被子View的绘画覆盖住,所以我们必须要在子View绘画完以后绘画,就是dispathDraw方法了,该方法主要是分发子View的绘画,我们的效果就在super.dispathDraw()后编写,这要就ok了~
至于绘画的详解,我们分STATE_SHOWFIX(显示大高达头像),STATE_PLAYANIM(显示动画),STATE_HIDEFIX(隐藏动画)。
详细解析这三种请看 事件 篇
事件
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if ((event.getRawX() > mDropRectF.left && event.getRawX() < mDropRectF.right) && (event.getRawY() > mDropRectF.top && event.getRawY() < mDropRectF.bottom) && mCurrentState != STATE_PLAYANIM) {
mDownX = event.getRawX();
mDownY = event.getRawY();
isClick = true;
mDowmTime = System.currentTimeMillis();
return true;
}
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getRawX();
float moveY = event.getRawY();
//点击事件
if (System.currentTimeMillis() - mDowmTime > ViewConfiguration.getTapTimeout() && Math.abs(mDownX - moveX) > 20 && Math.abs(mDownY - moveY) > 20) {//在这个事件内被认为不是点击事件
isClick = false;
mCurrentState = STATE_SHOWFIX;
updateDragBitmapPos(moveX, moveY);
}
break;
case MotionEvent.ACTION_UP:
if (isClick) {
if (mListener != null) {
mListener.onDropBimapClick(this);
}
return super.onTouchEvent(event);
}
if ((mDropRectF.centerX() > mFixRectF.left && mDropRectF.centerX() < mFixRectF.right) && (mDropRectF.centerY() > mFixRectF.top && mDropRectF.centerY() < mFixRectF.bottom)) {
mCurrentState = STATE_PLAYANIM;
if (mPlayAnimThread == null || !mPlayAnimThread.isAlive()) {
mPlayAnimThread = new PlayAnimThread();
}
switch (mThreadMode) {
case RESTART_THREAD:
mPlayAnimThread.canInvalidate(true);
if (!mPlayAnimThread.isAlive()) {
mPlayAnimThread.start();
}
break;
case NEW_THREAD:
mPlayAnimThread.start();
break;
}
if (mListener != null) {
mListener.onDropAndFixCoincidence(this);
}
}
restoreDropBitmap();
break;
}
return super.onTouchEvent(event);
}
当按下的时候,判断是否触摸到了 小高达头像,如果触摸到了,我们就消费这个事件,并获取按下的x,y坐标,和赋值isClick=true(是否是点击的标记,如果是就为true,否就为false),还有按下的时间dowTime,这些常量注意是用于判断是否为点击事件。
当手指移动的时候,通过ViewConfiguration的相关触发事件的规范常量进行判断是否为点击事件,getTapTimeout为一个触摸板触摸到释放可认为是一个点击事件而不是一个触摸移动手势的最大时间,就是说这个时间内进行一次触摸和释放操作就可以认为是一次点击事件,还有那个20的数值,是指一个触摸板在触摸释放之前可以移动的最大距离,就是说如果在这个距离之内就可以认为是一个点击事件,否则就是一个移动手势,这个数值在ViewConfiguration中可以通过getHoverTapSlop()虎丘的,但是,不知道为啥我的android studio居然显示没有这个方法,打开它源码查看居然也没有,奇怪了= =。。。
如果超出这个范围不认为是一个点击事件,isClick=false,标识为不是点击事件,mCurrentState是当前的绘画转态,STATE_SHOWFIX就是要绘画出大高达头像以及根据手机的移动坐标去进行小高达头像的拖拽效果。这个效果我交给updateDragBitmapPos()方法去处理,代码如下:
private void updateDragBitmapPos(float rawX, float rawY) {
float left = rawX - mDropBitmap.getWidth() / 2;
float top = rawY - mDropBitmap.getHeight() / 2;
float right = rawX + mDropBitmap.getWidth() / 2;
float bottom = rawY + mDropBitmap.getHeight() / 2;
float vLeft = getLeft();
float vRigth = getRight();
float vTop = getTop();
float vBootom = getBottom();
float rDistance = 0, bDistance = 0;
if (right >= vRigth) {
rDistance = right - vRigth;
}
if (bottom >= vBootom) {
bDistance = bottom - vBootom;
}
mDropRectF.set(left <= vLeft ? vLeft : left - rDistance, top <= vTop ? vTop : top - bDistance, right, bottom);
invalidate();
}
当手指抬起时,首先判断根据isClick标记判断是否为点击事件,是就回调接口对象的onDropBimapClick方法,随后return super.onTochEvent(event)。
如果不是点击事件,就认为是拖拽的结束,就将判断这个 小高达图片 是否在 大高达图片 范围中,如果是就将mCurrentState设置为STATE_PLAYANIM,表示播放图片动画。但是这个动画不是View的动画啊,没有办法startAnim等方法。。。,怎么办呢?应该很多大牛哥想到很多方法了,但小弟我想到的只有开启一条线程去执行了这个傻逼方法去做,不好意思,如果有更好的办法请评论一下,谢谢~~,请看代码:
/**
* 播放动画显示线程
*/
class PlayAnimThread extends Thread {
protected int times = 1;
protected boolean mInvadiate;
@Override
public void run() {
switch (mThreadMode) {
case NEW_THREAD:
times = 1;
while (times <= 5) {
try {
Thread.sleep(200);
postInvalidate();
} catch (InterruptedException e) {
e.printStackTrace();
}
times++;
}
mCurrentState = STATE_HIDEFIX;
break;
case RESTART_THREAD:
while (mThreadMode == RESTART_THREAD) {
if (!mInvadiate) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
if (times <= 5 && mInvadiate) {
try {
Thread.sleep(200);
postInvalidate();
} catch (InterruptedException e) {
e.printStackTrace();
}
times++;
} else {
mCurrentState = STATE_HIDEFIX;
mInvadiate = false;
times = 1;
}
}
break;
}
}
/**
* 是否可以绘制动画
*
* @return
*/
private void canInvalidate(boolean invalidate) {
this.mInvadiate = invalidate;
}
}
我想开启线程是一个耗时的操作又占用内存,那我就分两种模式去做,分别是:
NEW_THREAD:每次触发都开启一条线程,这个模式不占用内存,但速度较慢,适合不频繁触发这个事件的操作。
RESTART_THREAD:第一次触发就开启一条线程,该线程只有转换线程模式的时候才能结束,比较占用内存,但速度较快,适合频繁触发这个事件的操作。
动画结束以后,就将mCurrentState设置为STATE_HIDEFIX,表示隐藏 大高达头像。
在动画播放期间,触发接口对象的onDropAndFixCoincidence()方法进行回调事件,并调用restoreDropBitmap()将小高达头像恢复原位,代码如下:
private void restoreDropBitmap() {
mDropRectF.set(getWidth() / 2 - mDropBitmap.getWidth() / 2, getHeight() - mDropBitmap.getHeight() - mDropAndBottomDistance, getWidth() / 2 + mDropBitmap.getWidth() / 2, getHeight() - mDropAndBottomDistance);
invalidate();
}
如何使用
请看该控件的自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DropSwitchView">
<attr name="threadMode"><!--线程模式-->
<enum name="restart_thread" value="1" /><!--重复使用线程模式-->
<enum name="new_thread" value="2" /><!--new新线程模式,默认-->
</attr>
<attr name="arrayMode"><!--子View的排列方式-->
<enum name="avg_arrayMode" value="1" /><!--平均排列,默认-->
<enum name="free_arrayMode" value="2" /><!--自用排列-->
</attr>
<attr name="avgModeChildMargin" format="dimension" /><!--平均模式排列模式下的子View距离,默认为0px-->
<attr name="avgModeDBMargin" format="dimension" /><!--平均模式排列模式下的子View与拖动图片的间距,默认为0px-->
<attr name="avgModeBorderMargin" format="dimension" /><!--平均模式排列模式下的左右边距,默认为0px-->
<attr name="bottomBGHeight" format="dimension" /><!--背景高度,默认150px-->
<attr name="bottomBackGround" format="reference" /><!--背景图片-->
<attr name="dropAndBottomDistance" format="dimension" /><!--拖动图片离屏幕底部的距离,默认20px-->
<attr name="dropDrawable" format="reference" /><!--拖动图片-->
<attr name="fixDrawable" format="reference" /><!--固定在屏幕中间的图片-->
<attr name="resultDrawable" format="reference" /><!--显示动画的图片-->
</declare-styleable>
</resources>
还有xml布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="study.bin.dropswitchview.MainActivity">
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<study.bin.dropswitchview.view.DropSwitchView
android:id="@+id/dropSwitchView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:bottomBGHeight="70dp"
app:dropAndBottomDistance="20dp"
app:dropDrawable="@mipmap/drop"
app:fixDrawable="@mipmap/fix"
app:resultDrawable="@mipmap/show_result"
app:arrayMode="avg_arrayMode"
app:threadMode="restart_thread"
app:bottomBackGround="@drawable/bg"
app:avgModeBorderMargin="5dp"
app:avgModeChildMargin="5dp"
app:avgModeDBMargin="8dp"
>
<TextView
android:id="@+id/tv_bottom1"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="20dp"
android:layout_marginTop="10dp"
android:background="@android:color/holo_red_dark"
android:drawableTop="@mipmap/ic_launcher"
android:gravity="center"
android:text="你好1"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/tv_bottom2"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginRight="20dp"
android:layout_marginTop="10dp"
android:background="@android:color/holo_blue_dark"
android:drawableTop="@mipmap/ic_launcher"
android:gravity="center"
android:text="你好2"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/tv_bottom3"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginRight="20dp"
android:layout_marginTop="10dp"
android:background="@android:color/holo_green_light"
android:drawableTop="@mipmap/ic_launcher"
android:gravity="center"
android:text="你好3"
android:textColor="#FFFFFF" />
<TextView
android:id="@+id/tv_bottom4"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginRight="20dp"
android:layout_marginTop="10dp"
android:background="@android:color/holo_blue_light"
android:drawableTop="@mipmap/ic_launcher"
android:gravity="center"
android:text="你好4"
android:textColor="#FFFFFF" />
</study.bin.dropswitchview.view.DropSwitchView>
</FrameLayout>
到此,整个控件的解析到结束了。如果您有任何的问题,或者任何的建议,请留言,我若看见必定第一时间回复,谢谢~。还有…这个控件的名字我始终不知道怎么起名,如果有好的名字就评论一下吧,谢谢。
项目源码下载地址:
http://download.csdn.net/detail/qq_30321211/9707063