actionsheet是IOS中自带从屏幕下方弹出并且可点击的弹框控件,使用场景很广泛。在国内,越来越多的Android App开始模仿IOS的效果。这不,产品经理就看中这个效果,需要来实现
之前我采用过View的形式来实现,今天我们换一种实现形式,改用Fragment来实现,有一点要注意了,我们不采用Fragment直接加载一个视图,而是在DecorView上面绘制一个视图,并且由FragmentManager对这个View进行管理
本文的代码已上传至Github,欢迎大家star、follow
知识点
简单罗列一下开发过程中的几个重要知识点
- DecorView
- Fragment的管理
- GridLayout的使用
- 自定义属性的优先级
基本功能的实现
首先看看xml的布局
参考之前的截图,我采用线性布局,顶部是功能区域,底部就一个取消按钮。其中功能区域分Title声明部分以及ListView、GridLayout两种不同的展示效果
再看看Fragment的结构,我打算采用Build模式创建一个Fragment出来
public class AndroidActionSheetFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
public static class Builder {
FragmentManager fragmentManager;
public Builder(FragmentManager fragmentManager) {
this.fragmentManager=fragmentManager;
}
}
}
下面定义几个变量,供Fragment使用
//默认tag,用来校验fragment是否存在
String tag="ActionSheetFragment";
//ActionSheet的Title
String title="title";
//ActionSheet上ListView或者GridLayout上相关文字、图片
String[] items;
int[] images;
//ActionSheet点击后的回调
OnItemClickListener onItemClickListener;
//点击取消之后的回调
OnCancelListener onCancelListener;
//提供类型,用以区分ListView或者GridLayout
CHOICE choice;
public enum CHOICE {
ITEM, GRID
}
随后就是一系列的set
public Builder setTag(String tag) {
this.tag = tag;
return this;
}
public Builder setTitle(String title) {
this.title = title;
return this;
}
public Builder setItems(String[] items) {
this.items = items;
return this;
}
public Builder setImages(int[] images) {
this.images = images;
return this;
}
public Builder setChoice(CHOICE choice) {
this.choice = choice;
return this;
}
public Builder setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
return this;
}
public Builder setOnCancelListener(OnCancelListener onCancelListener) {
this.onCancelListener = onCancelListener;
return this;
}
Build里面根据不同展现类型进行调用
public void show() {
AndroidActionSheetFragment fragment;
if (choice==CHOICE.ITEM) {
fragment=AndroidActionSheetFragment.newItemInstance(title, items);
fragment.setOnItemClickListener(onItemClickListener);
fragment.setOnCancelListener(onCancelListener);
fragment.show(fragmentManager, tag);
}
if (choice==CHOICE.GRID) {
fragment=AndroidActionSheetFragment.newGridInstance(title, items, images);
fragment.setOnItemClickListener(onItemClickListener);
fragment.setOnCancelListener(onCancelListener);
fragment.show(fragmentManager, tag);
}
}
public static AndroidActionSheetFragment newItemInstance(String title, String[] items) {
AndroidActionSheetFragment fragment=new AndroidActionSheetFragment();
Bundle bundle=new Bundle();
bundle.putString("title", title);
bundle.putStringArray("items", items);
bundle.putInt("type", 1);
fragment.setArguments(bundle);
return fragment;
}
public static AndroidActionSheetFragment newGridInstance(String title, String[] items, int[] images) {
AndroidActionSheetFragment fragment=new AndroidActionSheetFragment();
Bundle bundle=new Bundle();
bundle.putString("title", title);
bundle.putStringArray("items", items);
bundle.putIntArray("images", images);
bundle.putInt("type", 2);
fragment.setArguments(bundle);
return fragment;
}
public static AndroidActionSheetFragment.Builder build(FragmentManager fragmentManager) {
AndroidActionSheetFragment.Builder builder= new AndroidActionSheetFragment.Builder(fragmentManager);
return builder;
}
到这一步,正常的Fragment就已经搭建完成了。如果是一般情况下放到某一个容器中的Fragment,是可以直接展现在我们面前的,但是这里我们应该怎么样去创建出这样一个视图出来呢?这里需要了解什么是DecorView
- DecorView为整个Window界面的最顶层View。
- DecorView只有一个子元素为LinearLayout。代表整个Window界面。
- LinearLayout里有两个FrameLayout子元素。 一个为标题栏显示界面,另一个为内容栏显示界面。就是setContentView()方法载入的布局界面
盗图说明
只要我们将View加在其上面,就能显示出来了
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
InputMethodManager manager= (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
if (manager.isActive()) {
View focusView=getActivity().getCurrentFocus();
manager.hideSoftInputFromWindow(focusView.getWindowToken(), 0);
}
realView=inflater.inflate(R.layout.view_actionsheet, container, false);
initViews(realView);
decorView=getActivity().getWindow().getDecorView();
((ViewGroup) decorView).addView(realView);
return super.onCreateView(inflater, container, savedInstanceState);
}
最后就是去把fragment加到Stack里面了。我们这里实现一下如何展现fragment跟销毁fragment
private void show(final FragmentManager manager, final String tag) {
if (manager.isDestroyed() || !isDismiss) {
return;
}
isDismiss=false;
new Handler().post(new Runnable() {
@Override
public void run() {
FragmentTransaction transaction=manager.beginTransaction();
transaction.add(ActionSheetFragment.this, tag);
transaction.addToBackStack(null);
transaction.commitAllowingStateLoss();
}
});
}
这里就是将fragment放到Stack里面去。有2个地方简单说明以下
- 使用Stack管理,addToBackStack入栈popBackStack出栈
- 采用commitAllowingStateLoss是为了防止你在Activity存储状态之后(OnSaveInstance)调用的commit造成崩溃,故不用commit
private void dismiss() {
if (isDismiss) {
return;
}
isDismiss=true;
new Handler().post(new Runnable() {
@Override
public void run() {
getChildFragmentManager().popBackStack();
FragmentTransaction transaction=getFragmentManager().beginTransaction();
transaction.remove(AndroidActionSheetFragment.this);
transaction.commitAllowingStateLoss();
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
((ViewGroup) decorView).removeView(realView);
}
}, 500);
}
这里就是将fragment从Stack里推出并且移除,并且在移除之后执行onDestroyView方法删除decorview视图
来看看目前的效果
AndroidActionSheetFragment.build(getSupportFragmentManager())
.setChoice(AndroidActionSheetFragment.Builder.CHOICE.ITEM).setTitle("标题").setTag("MainActivity")
.setItems(new String[]{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"})
.setOnItemClickListener(new AndroidActionSheetFragment.OnItemClickListener() {
@Override
public void onItemClick(int position) {
}
}).show();
后续效果
我们完成了基本功能,弹出、关闭,现在要开始进行一些处理工作
- 高度控制
我们的listView的高度在item数量比较少的情况下是比较完美的,那么如果数量比较多怎么办?特别是产品经理说,最多只能显示4个item,这个我们怎么处理?我们就需要动态的去修改ListView或者GridLayout的高度,这里我们仅仅简单的控制ListView的高度不超过状态栏的高度来说明问题
ListView pop_listview= (ListView) view.findViewById(R.id.pop_listview);
LinearLayout.LayoutParams params= (LinearLayout.LayoutParams) pop_listview.getLayoutParams();
int maxHeight=getScreenHeight(getActivity())-getStatusBarHeight(getActivity())-dp2px(getActivity(), (45+10+10))-dp2px(getActivity(), (45+0.5f));
int dateHeight=dp2px(getActivity(), (45+0.5f)*getArguments().getStringArray("items").length);
if (maxHeight
这里解释一下“死值”的由来:我偷懒直接将“取消”的marginTop以及marginBottom值设置成10,并且每一个item的高度是45dp,divide是0.5dp
- 动画效果
目前的效果太过于生硬了,直来直往了一点,我们需要从底部升到上面来,怎么做了?我们利用属性动画来实现
private void startPlay() {
pop_child_layout.post(new Runnable() {
@Override
public void run() {
final int moveHeight=pop_child_layout.getMeasuredHeight();
ValueAnimator valueAnimator=ValueAnimator.ofFloat(0, 1);
valueAnimator.setDuration(500);
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
pop_child_layout.setVisibility(View.VISIBLE);
}
});
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
ArgbEvaluator argbEvaluator=new ArgbEvaluator();
realView.setBackgroundColor((Integer) argbEvaluator.evaluate(animation.getAnimatedFraction(), Color.parseColor("#00000000"), Color.parseColor("#70000000")));
//当底部存在导航栏并且decorView获取的高度不包含底部状态栏的时候,需要去掉这个高度差
if (getNavBarHeight(pop_child_layout.getContext())>0 && decorView.getMeasuredHeight()!=getScreenHeight(pop_child_layout.getContext())) {
pop_child_layout.setTranslationY((moveHeight+getNavBarHeight(pop_child_layout.getContext()))*(1-animation.getAnimatedFraction())-getNavBarHeight(pop_child_layout.getContext()));
}
else {
pop_child_layout.setTranslationY(moveHeight*(1-animation.getAnimatedFraction()));
}
}
});
valueAnimator.start();
}
});
}
private void stopPlay() {
pop_child_layout.post(new Runnable() {
@Override
public void run() {
final int moveHeight=pop_child_layout.getMeasuredHeight();
ValueAnimator valueAnimator=ValueAnimator.ofFloat(0, 1);
valueAnimator.setDuration(500);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
ArgbEvaluator argbEvaluator=new ArgbEvaluator();
realView.setBackgroundColor((Integer) argbEvaluator.evaluate(animation.getAnimatedFraction(), Color.parseColor("#70000000"), Color.parseColor("#00000000")));
if (getNavBarHeight(pop_child_layout.getContext())>0 && decorView.getMeasuredHeight()!=getScreenHeight(pop_child_layout.getContext())) {
pop_child_layout.setTranslationY((moveHeight+getNavBarHeight(pop_child_layout.getContext()))*animation.getAnimatedFraction()-getNavBarHeight(pop_child_layout.getContext()));
}
else {
pop_child_layout.setTranslationY(moveHeight*animation.getAnimatedFraction());
}
}
});
valueAnimator.start();
}
});
}
这里有一个地方要注意一下,
在不同的系统版本下,虽然同时有底部虚拟导航条,但是展现出来的效果不一样,所以我们需要对DecorView进行一下处理,判断一下其高度不是等同于顶部导航条的高度。如果相等,我们就要把NavigationView的高度给去除掉进行判断
我们在onCreateView中添加startPlay()方法,同时在onDestoryView中添加stopPlay()方法。pop_child_layout默认为INVISIBLE状态,在动画执行过程中VISIBLE
现在来看看最终效果
- 自定义属性的设置
刚才说了这么多,我随便指出一个问题,你这边颜色什么的都帮我设置好了,如果我没有你的源码,我怎么才能改文本颜色还有字体大小呢?这样我们就需要提供一些自定义参数的设置。这里没有办法使用xml进行设置,我们只能把效果加在Theme上了
定义了2个属性:颜色与字体大小,同时定义了供Theme使用的变量
在使用过程中,直接在主题加上了myThemeStyle属性
最后在Adapter中这样使用
TypedArray array=context.obtainStyledAttributes(null, R.styleable.SheetParams, R.attr.myThemeStyle, 0);
array.recycle();
holder.pop_desp.setTextColor(array.getColor(R.styleable.SheetParams_textColor, Color.BLACK));
holder.pop_desp.setTextSize(array.getDimensionPixelSize(R.styleable.SheetParams_textSize, 10));
这里有个属性优先级的情况。刚才我们是在xml中配置了style的属性,但是有的人希望直接在onCreate之前调用setTheme来改变属性,那么此时如果你在xml中也配置了一份相同的属性,那么你setTheme将不起作用。另外还有在xml中的view直接设置自定义属性以及设置style,这2个优先级更高
OK,我该说的都说完了。通过本篇的学习,你是不是获益匪浅?