Android——自定义镂空遮盖控件
Posted 安卓笨笨鸟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android——自定义镂空遮盖控件相关的知识,希望对你有一定的参考价值。
刚学完ViewDragHelper和PorterDuffXferMode的我,突然想做一个这样效果的自定义控件:点击ListView的列表项,通过ViewDragHelper用动画方式上下各弹出一个控件遮盖住ListView,这两个控件在遮盖listView的过程中有一部分是镂空的。先上效果图:
首先是进行页面的布局,让自定义控件PlayLayout继承自Franlayout,在最底层放的就是listView所在的子FramLayout(Id:midContent),然后依次在上面加上下两个看起来被分割的FrameLayout(这里直接写了自定义控件SplitLayout继承Framlayout,id分别为:topSplit,bottomSplit),最后还要放上旋转圆的部分(自定义控件ControlPanel,)
大致布局像这样:
<PlayLayout extends frameLayout id:palyLayout>
<FrameLayout id:midContent>//显示ListView的底层部分
</FrameLayout>
<SplitLayout extends FrameLayout id:topSplit>//顶部的分割区
</SplitLayout>
<SplitLayout extends FrameLayout id:bootomSplit>//底部分割
</SplitLayout>
<ControlPanel extends FrameLayout id:controlPanel>//旋转圆部分
</ControlPanel>
</PlayLayout>
首先分析怎么设置上下SplitLayout弹出的工作:
1、在PlayLayout中的onFinishInflate()中获得各个子布局
midContent = (ViewGroup) getChildAt(0);
topSplit = (SplitLayout) getChildAt(1);
bottomSplit = (SplitLayout) getChildAt(2);
controlPanel = (ControlPanel) getChildAt(3);
//SplitLayout.Position是一个enum 标志该SplitLayout是上半部分还是下半部分,默认下半部分
topSplit.setPosition(SplitLayout.Position.TOP);
2、根据可以设置的splitScale即上下splitLayout高度比(默认0.5,上下各一半),在PlayLayout控件的onMeaure中设置他们的高度
int count = getChildCount();
for (int i = 0; i < count; i++) {
//测量子控件
measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
}
int w = MeasureSpec.getSize(widthMeasureSpec);
int h = MeasureSpec.getSize(heightMeasureSpec);
//根据分割比例计算上下分割控件各自的高度
int topH = (int) (h * 1.0f * splitScale + 0.5f);
int bottomH = h - topH;
ViewGroup.LayoutParams params1 = topSplit.getLayoutParams();
ViewGroup.LayoutParams params2 = bottomSplit.getLayoutParams();
ViewGroup.LayoutParams params3 = controlPanel.getLayoutParams();
params1.height = topH;
params1.width = getMeasuredWidth();
params2.height = bottomH;
params2.width = getMeasuredWidth();
params3.height = (int) (radias*2);
params3.width = (int) (radias*2) ;
//设置各自的高度
topSplit.setLayoutParams(params1);//上半部分割控件
bottomSplit.setLayoutParams(params2);//下半部分
controlPanel.setLayoutParams(params3);//旋转圆盘
3.重写PlayLayout的onLayout方法,进行子控件的布局
int h = getMeasuredHeight();
int topH = (int) (h * 1.0f * splitScale + 0.5f);
int bottomH = h - topH;
//放置listview的底层的midContent正常的layout
midContent.layout(left, top, right, bottom);
//topSplit一开始我们让其在最上面隐藏,点击listItem后才出来
topSplit.layout(left, -topH, right, 0);
//同样,bottomSplit最开始让其在父控件的底部隐藏
bottomSplit.layout(left, h, right, h + bottomH);
controlPanel.layout((int)(getMeasuredWidth()*1.0/2-radias),
(int)(topH-radias),
(int)(getMeasuredWidth()*1.0/2+radias),
(int) (topH+ radias));
4.通过ViewDragHelper来处理滚动,接下来是ViewDragHelper的一般过程
先初始化
public enum Status {//一共三个状态,打开,关闭,正在拖动
OPEN, DRAGING, CLOSE
}
private Status status = Status.CLOSE;
private ViewDragHelper mDragHelp;
//初始化
private void initView() {
mDragHelp = ViewDragHelper.create(this, mDragCallbak);
status = Status.CLOSE;//初始化状态,splitLayout和controlPanel处于关闭状态
}
当size发生变化时,重置一些参数
//这里是设置底部的splitLayout可以拖拽
private int mTop;//当前拖拽的位置
private int mStartTop;//初始Y方向top位置
private int mEndTop;//打开后,拖拽到的位置
private int mRange;//可以拖动的范围
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int hh = getMeasuredHeight();
int topH = (int) (hh * 1.0f * splitScale + 0.5f);
mStartTop = hh;//最开始是在底部
mEndTop = topH;//能够最终拖拽到父控件顶部splitLayout高度大小的位置
mTop = hh;//初始化当前位置
mRange = mStartTop - mEndTop;//计算拖拽范围
}
拦截时间,以及处理事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (status == Status.CLOSE) return false;
return mDragHelp.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragHelp.processTouchEvent(event);
return true;
}
初始化ViewDragHelper.Callback
private ViewDragHelper.Callback mDragCallbak = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == bottomSplit;//选择能够拖动的控件
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//设置当前的拖拽位置,要在拖动范围之内
if (top > mStartTop)
top = mStartTop;
if (top < mEndTop)
top = mEndTop;
return top;
}
//当拖动的控件位置发生变化时,调用该函数,在函数里更新当前的top位置mTop
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
mTop += dy;
mTop = Math.max(Math.min(mTop, mStartTop), mEndTop);
dispatchScrollEvent();
}
//设置拖动范围
@Override
public int getViewVerticalDragRange(View child) {
return mRange;
}
//当拖拽放开时
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (mTop < mEndTop + mRange / 2) {
open();
} else {
close();
}
ViewCompat.postInvalidateOnAnimation(PlayLayout.this);
}
};
还需要重写onComputeScroll(和Scroll.startScrolll方法一样)
@Override
public void computeScroll() {
super.computeScroll();
if (mDragHelp.continueSettling(true))
ViewCompat.postInvalidateOnAnimation(this);
}
ViewDragHelper的惯性操作都放在open和close
public void open() {
mDragHelp.smoothSlideViewTo(bottomSplit, 0, mEndTop);
ViewCompat.postInvalidateOnAnimation(this);
}
private void close() {
mDragHelp.smoothSlideViewTo(bottomSplit, 0, mStartTop);
ViewCompat.postInvalidateOnAnimation(this);
}
其中在ViewDragHelper.Callback的onViewPositionChanged中每次调用我们都计算了当前top,并分发了事件:
private Status updateStatus() {
if (mTop <= mEndTop + 5) {
status = Status.OPEN;
System.out.println("status->>" + "open");
} else if (mTop >= mStartTop - 5) {
status = Status.CLOSE;
System.out.println("status->>" + "close");
} else {
status = Status.DRAGING;
}
return status;
}
private void dispatchScrollEvent() {
updateStatus();//根据当前mTop计算状态
//计算拖拽完成的程度
float percent = (mStartTop - mTop) * 1.0f / mRange;
//根据百分比,移动呈现顶部splitLayout
ViewHelper.setTranslationY(topSplit, percent * 1.0f * mEndTop + 0.5f);
ViewHelper.setScaleY(controlPanel, percent);
ViewHelper.setScaleX(controlPanel, percent);
ViewHelper.setRotation(controlPanel, (1 - percent) *360);
}
5.通过PorterDuffXferMode遮罩层设置镂空的SplitLayout
这里是在SplitLayout控件中
首先初始化
private PorterDuffXfermode mMode;
private Paint mPaint;
private Bitmap mBg;//控件背景
private void initView() {
mPaint = new Paint();
mPaint.setAlpha(0);//镂空遮罩层画笔一定要透明
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
}
然后重写onDraw时这样处理
int w = getMeasuredWidth();
int h = getMeasuredHeight();
Bitmap bm = null;
if (mBg != null) {
int bitW = mBg.getWidth();
int bitH = mBg.getHeight();
//因为该控件只显示父控件一定比例的高度,所以背景比例是按父控件高度计算的
float scaleH = h * 1.0f / splitScale / bitH;//h*(1/splitScale)得到父控件高度
float scaleW = w * 1.0f / bitW;
Matrix matrix = new Matrix();
matrix.setScale(scaleW, scaleH);
bm = Bitmap.createBitmap(mBg, 0, 0, bitW, bitH, matrix, true);
}
int startDegree = mPosition == Position.TOP ? 180 : 0;
//利用同样大小的bitMap做镂空层
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap);
//根据Position决定绘制已经缩放图像的上半还是下半,在下半则起点为父控件高度减去自己的高度
int t = mPosition == Position.TOP ? 0 : (int) (h * 1.0f / splitScale - h);
if (mBg == null)
canvas1.drawColor(Color.argb(200, 128, 128, 128));
else
canvas1.drawBitmap(bm,
new Rect(0, t, w, t + h),
new Rect(0, 0, w, h),
null);
//设置镂空圆的位置
float top = h - radias;
if (mPosition == Position.BOTTOM)
top = -radias;
float bottom = top + 2 * radias;
RectF rectF = new RectF(w / 2 - radias, top, w / 2 + radias, bottom);
//上面再bitmap上已经画了背景图片,再通过带xferMode属性的透明画笔去镂空背景图片,让其显示控件下面一层的控件视图
canvas1.drawArc(rectF, startDegree, 180, true, mPaint);//注意mPaint的特殊设置,alpha=0,xmode(Mode.SRC_In);
//将镂空层画到背景上
canvas.drawBitmap(bitmap, 0, 0, null);
旋转圆控件的onDraw处理和SplitLayout基本一样,除了要镂空
int bitW = mBg.getWidth();
int bitH = mBg.getHeight();
System.out.println("sout->>bitH=" + bitH + " bitW=" + bitW);
//计算缩放比例
float scaleH = h * 1.0f / bitH;
float scaleW = w * 1.0f / bitW;
Matrix matrix = new Matrix();
matrix.setScale(scaleW, scaleH);
Bitmap bitmap=Bitmap.createBitmap(mBg,0,0,bitW,bitH,matrix,true);
mShader=new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);//通过BitmapShader很容易自定义各种什么圆角,圆形背景控件
mPaint.setShader(mShader);
canvas.drawCircle(w/2,h/2,radias,mPaint);
注意:要想实现镂空还得,必须在layout:xml布局文件里面指定background如下:
<com.fan.skymusic.SplitLayout
android:background="#00ffffff"
android:id="@+id/topSplit"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.fan.skymusic.SplitLayout>
这里也有个问题搞不懂?
当我这这样设置时镂空的地方成了黑色,也不显示下层控件
<com.fan.skymusic.SplitLayout
android:background="#ffffff"
android:alpha="0"
android:id="@+id/topSplit"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.fan.skymusic.SplitLayout>
(android:background=”#00ffffff”)和(android:background=”#ffffff” android:alpha=”0”) 不应该是一样的吗
demo源代码地址(github新手,文件在app module 里面):
https://github.com/yifantao/SkyMusic
以上是关于Android——自定义镂空遮盖控件的主要内容,如果未能解决你的问题,请参考以下文章