原来这么简单就可以实现ActionSheet了

actionsheet是IOS中自带从屏幕下方弹出并且可点击的弹框控件,使用场景很广泛。在国内,越来越多的Android App开始模仿IOS的效果。这不,产品经理就看中这个效果,需要来实现

原来这么简单就可以实现ActionSheet了_第1张图片
ActionSheet

之前我采用过View的形式来实现,今天我们换一种实现形式,改用Fragment来实现,有一点要注意了,我们不采用Fragment直接加载一个视图,而是在DecorView上面绘制一个视图,并且由FragmentManager对这个View进行管理

本文的代码已上传至Github,欢迎大家star、follow

知识点

简单罗列一下开发过程中的几个重要知识点

  1. DecorView
  2. Fragment的管理
  3. GridLayout的使用
  4. 自定义属性的优先级

基本功能的实现

首先看看xml的布局


原来这么简单就可以实现ActionSheet了_第2张图片
布局结构

参考之前的截图,我采用线性布局,顶部是功能区域,底部就一个取消按钮。其中功能区域分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

  1. DecorView为整个Window界面的最顶层View。
  2. DecorView只有一个子元素为LinearLayout。代表整个Window界面。
  3. LinearLayout里有两个FrameLayout子元素。 一个为标题栏显示界面,另一个为内容栏显示界面。就是setContentView()方法载入的布局界面

盗图说明


原来这么简单就可以实现ActionSheet了_第3张图片
DecorView

只要我们将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个地方简单说明以下

  1. 使用Stack管理,addToBackStack入栈popBackStack出栈
  2. 采用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();
        }
    });
}

这里有一个地方要注意一下,

原来这么简单就可以实现ActionSheet了_第4张图片
虚拟导航栏效果偏差

在不同的系统版本下,虽然同时有底部虚拟导航条,但是展现出来的效果不一样,所以我们需要对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,我该说的都说完了。通过本篇的学习,你是不是获益匪浅?

你可能感兴趣的:(原来这么简单就可以实现ActionSheet了)