这俩周在做发表的相册改版,今天总结一下其中用到的一些技术点
1、android:clipToPadding属性的妙用
属性的解释:
Defines whether the ViewGroup will clip its drawing surface so as to exclude the padding area.
定义了是否允许ViewGroup在padding中绘制。
该属性的默认值是true,即不允许。
使用场景:
1、设置ListView(或GridView等)首行或尾行距离顶部或底部有一段距离(padding),且在滑动时padding部分仍看到ListView内容的效果。
例:
android:layout_width="match_parent"
android:layout_height=“match_parent"
android:clipToPadding="false"
android:paddingTop="70dip"
android:paddingBottom="70dip"
/>
若不设置clipPadding为false,我们会直接看到ListView头部和尾部占有70dp的padding,且在滑动过程中padding始终存在,且在padding部分看不到ListView的内容。
若设置clipPadding为false,滑动过程中在padding部分可以看到ListView的内容,且有滑动到顶部才出现paddingTop 70dp,滑动到底部才出现paddingBottom 70dp的效果。
以前为了达到这个效果,解决方法可能是在首行或尾行加上一个隐藏的View,或者是加HeaderView\FooterView来实现。比较麻烦。
2、与ViewPager结合实现画廊效果
画廊效果:每次滑动只滑动一页+ 滑动item居中 + 一屏显示多个item
//缓存 3屏 左右拖动可以看到前后的图片mViewPager.setOffscreenPageLimit(3);
//设置item之间的间距
//int pageMargin = ScreenTools.instance(this).dip2px(-15);
//mViewPager.setPageMargin(pageMargin);
intpadding = ScreenTools.instance(this).dip2px(40);
mViewPager.setPadding(padding,0,padding,0);
mViewPager.setClipToPadding(false);
效果图如下:
3、网上有用android:clipChildren实现画廊效果http://www.trinea.cn/android/viewpager-multi-fragment-effect/
android:clipChildren
属性解释:
Defines whether a child is limited to draw inside of its bounds or not.
用来定义他的子控件是否要在他应有的边界内进行绘制。
默认值为true,即不允许进行扩展绘制。
2、ViewDragHelper
ViewDragHelper 是v4包中提供的一个用于解决界面控件拖拽移动问题的类,用这个类编写自定义的ViewGroup,可很大程度上简化了手势处理过于复杂的问题。自己去处理onInterceptTouchEvent和onTouchEvent这两个方法真的是一件很不容易的事
ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number
of useful operations and state tracking for allowing a user to drag and reposition
views within their parent ViewGroup.
ViewDragHepler基本用法:
1、获取ViewDragHelper的实例
ViewDragHelper.create(ViewGroup forParent, float sensitivity, ViewDragHelper.Callback cb);
参数1: 一个ViewGroup, 也就是ViewDragHelper将要用来拖拽谁下面的子view
参数2:灵敏度,一般设置为1.0f就行
参数3:一个回调,用来处理拖动到位置
2、继承ViewDragHelper.Callback类,该类有个抽象方法:tryCaptureView(View view, int pointerId) 表示尝试捕获子view,这里一定要返回true, 返回true表示允许。
3、重写两个方法Callback中int clampViewPositionHorizontal(View child, int left, int dx)和intclampViewPositionVertical(View child, int left, int dx) 这两个方法分别用来处理x方向和y方向的拖动的,返回值该child现在的位置。使用时要具体处理例如防止view超出边界等。
4、重写了Callback中的onViewReleased,处理放手后的操作,例根据滑动方向及滑动距离等调用settleCapturedViewAt方法回到指定的位置,settleCapturedViewAt后要调用invalidate,因为其内部使用的是mScroller.startScroll,所以别忘了需要invalidate()以及结合computeScroll方法一起。
5、重写Callback中getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0view的onClick事件才能执行。(如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)都是直接进入onTouchEvent,在onTouchEvent的DOWN的时候就确定了captureView。如果消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,只有这两个方法返回大于0的值才能正常的捕获。)
6、重写ViewGroup的onInterceptTouchEvent(MotionEvent ev)用来拦截事件
7、重写ViewGroup的onTouchEvent(MotionEvent event) 在这里面只要做两件事:mDragHelper.processTouchEvent(event);处理拦截到的事件,这个方法会在返回前分发事件;return true 表示消费了事件。
简单的几步就可以实现一个可以任意拖动到view了。
例子:
相片选择模块中底部的上下拖动抽屉效果及画廊中图片的拖动上滑删除功能都是用ViewDragHelper来实现的
效果图:画廊拖动上滑删除图片
效果图:底部上拉抽屉
3、PopupWindow setBackgroundDrawbale()
PopupWindow大家经常用,类似这样
效果:点击Popwindow外部区域可让其消失
mAlbumPopupWindow=newPopupWindow(contentView,ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT,true);
mAlbumPopupWindow.setBackgroundDrawable(newColorDrawable(Color.TRANSPARENT));//加上这行的效果是点popupwindow的外边也能dismiss
mAlbumPopupWindow.setOutsideTouchable(true);//外部区域可点击
mAlbumPopupWindow.setTouchable(true);//window 可点击
mAlbumPopupWindow.setFocusable(true);//获得焦点 内部可点击
有个点:PopupWindow setBackgroundDrawbale() 有什么作用?
为什么去掉这行,点击外部区域,Popupwindow就不会消失呢?
解惑:
如果有背景mBackground,则会在contentView外面包一层PopupViewContainer之后作为mPopupView,如果没有背景,则直接用contentView作为mPopupView。
而这个PopupViewContainer是一个内部私有类,它继承了FrameLayout,在其中重写了Key和Touch事件的分发处理。如果不设背景,由于PopupView本身并没有重写Key和Touch事件的处理,所以如果没有包这个外层容器类,点击Back键或者外部区域是不会导致弹框消失的。
见源码:
/**
*
Prepare the popup by embedding in into a new ViewGroup if the
* background drawable is not null. If embedding is required, the layout
* parameters' height is modified to take into account the background's
* padding.
*
*@parampthe layout parameters of the popup's content view
*/
private voidpreparePopup(WindowManager.LayoutParams p) {
if(mContentView==null||mContext==null||mWindowManager==null) {
throw newIllegalStateException("You must specify a valid content view by "
+"calling setContentView() before attempting to show the popup.");
}
if(mBackground!=null) {
finalViewGroup.LayoutParams layoutParams =mContentView.getLayoutParams();
intheight = ViewGroup.LayoutParams.MATCH_PARENT;
if(layoutParams !=null&&
layoutParams.height== ViewGroup.LayoutParams.WRAP_CONTENT) {
height = ViewGroup.LayoutParams.WRAP_CONTENT;
}
// when a background is available, we embed the content view
// within another view that owns the background drawable
PopupViewContainer popupViewContainer =newPopupViewContainer(mContext);
PopupViewContainer.LayoutParams listParams =newPopupViewContainer.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,height
);
popupViewContainer.setBackground(mBackground);
popupViewContainer.addView(mContentView,listParams);
mPopupView= popupViewContainer;
}else{
mPopupView=mContentView;
}
mPopupView.setElevation(mElevation);
mPopupViewInitialLayoutDirectionInherited=
(mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
mPopupWidth= p.width;
mPopupHeight= p.height;
}
private classPopupViewContainerextendsFrameLayout {
private static finalStringTAG="PopupWindow.PopupViewContainer";
publicPopupViewContainer(Context context) {
super(context);
}
@Override
protected int[] onCreateDrawableState(intextraSpace) {
if(mAboveAnchor) {
// 1 more needed for the above anchor state
final int[] drawableState =super.onCreateDrawableState(extraSpace +1);
View.mergeDrawableStates(drawableState,ABOVE_ANCHOR_STATE_SET);
returndrawableState;
}else{
return super.onCreateDrawableState(extraSpace);
}
}
@Override
public booleandispatchKeyEvent(KeyEvent event) {
if(event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if(getKeyDispatcherState() ==null) {
return super.dispatchKeyEvent(event);
}
if(event.getAction() == KeyEvent.ACTION_DOWN
&& event.getRepeatCount() ==0) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if(state !=null) {
state.startTracking(event,this);
}
return true;
}else if(event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if(state !=null&& state.isTracking(event) && !event.isCanceled()) {
dismiss();
return true;
}
}
return super.dispatchKeyEvent(event);
}else{
return super.dispatchKeyEvent(event);
}
}
@Override
public booleandispatchTouchEvent(MotionEvent ev) {
if(mTouchInterceptor!=null&&mTouchInterceptor.onTouch(this,ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
@Override
public booleanonTouchEvent(MotionEvent event) {
final intx = (int) event.getX();
final inty = (int) event.getY();
if((event.getAction() == MotionEvent.ACTION_DOWN)
&& ((x <0) || (x >= getWidth()) || (y <0) || (y >= getHeight()))) {
dismiss();
return true;
}else if(event.getAction() == MotionEvent.ACTION_OUTSIDE) {
dismiss();
return true;
}else{
return super.onTouchEvent(event);
}
}
@Override
public voidsendAccessibilityEvent(inteventType) {
// clinets are interested in the content not the container, make it event source
if(mContentView!=null) {
mContentView.sendAccessibilityEvent(eventType);
}else{
super.sendAccessibilityEvent(eventType);
}
}
}
}