自定义菜单收起展开动画(二)

本文介绍了一个自定义菜单控件的封装过程,包括Adapter的实现、点击事件处理及动画效果。通过封装,简化了菜单控件的使用,提高了代码复用性和维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天周一,新的一周,水的开始。

上次发完博客后,自己在项目中使用不是特别的方便,除了要写很多布局之外,还要写诸多的点击事件的监听,不是非常的方便,于是花了一天的时间封装了一下。

首先我们先缕缕思路,首先我想实现一个自定义的控件可以实现全部的父子菜单的功能,所以在原有的adapter基础上,我们需要增加累死expanedlistview类似的adapter的实现。

public abstract class SettingsAdapter<T> {
    private Context context;
    private List<T> mParentList;
    private List<List<T>> mChildList;

    public SettingsAdapter(Context context, List<T> mParentList, List<List<T>> mChildList) {
        this.context = context;
        this.mParentList = mParentList;
        this.mChildList = mChildList;
    }
    public int getParentCount(){
        return  mParentList==null?0:mParentList.size();
    }

    public T getItem(int position){
        return  mParentList.get(position);
    }
    public int getChildCount(int parentPosition){
        return mChildList==null?0: mChildList.get(parentPosition).size();
    }
    public T getChildItem(int parentPosition,int childPosition){
        return mChildList.get(parentPosition).get(childPosition);
    }
    public abstract View getParentView(View view,int position);
    public abstract View getChildView(View view,int parentPosition,int childPosition);

    public interface onDataChanged {
        void changed();
    }

    public void setOnDataChanged(onDataChanged onDataChanged) {
        this.onDataChanged = onDataChanged;
    }

    public onDataChanged onDataChanged;

    public void notifyDataSetChanged(){
        onDataChanged.changed();
    }
}
这次的adapter和上次的adapter大致上是一样的,不过是多了父菜单的view,以便于我们对于父布局的定制。

然后我们在原来的SettingView上进行修改,这次我们因为要实现的是一整个的菜单,所以依然是继承linearlayout,在初始化中设置方向为竖向。

 public SettingsView(Context context) {
        this(context, null);
    }

    public SettingsView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public SettingsView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context=context;
        setOrientation(VERTICAL);
    }
然后我们在SettingView中实现setAdapter这个接口,和上个博客的写法大致一样。

public void setAdapter(SettingsAdapter adapter){
        this.adapter=adapter;
        changeAdapter();
    }
下面我们重点看一下changAdapter里面的方法。

 private void changeAdapter() {
        removeAllViews();
        SettingsAdapter settingsAdapter=this.adapter;
        for(int i=0;i<settingsAdapter.getParentCount();i++){
            final View parentView = settingsAdapter.getParentView(this, i);
            parentView.setTag(i);
            addView(parentView);
            parentView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(onItemClickListener!=null){
                        onItemClickListener.ParentClicked((Integer) parentView.getTag());
                    }
                }
            });
            innerLayout=new LinearLayout(context);
            LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            innerLayout.setLayoutParams(lp);
            innerLayout.setOrientation(LinearLayout.VERTICAL);
            for(int j=0;j<settingsAdapter.getChildCount(i);j++){
                final View childView = settingsAdapter.getChildView(this, i, j);
                childView.setTag(String.valueOf(i) + String.valueOf(j));
                childView.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if(onItemClickListener!=null){
                            String tag= (String) childView.getTag();
                            onItemClickListener.childClicked(TagToInt(tag,0,1),TagToInt(tag,1,2));
                        }
                    }
                });
                innerLayout.addView(childView);
            }
            addView(innerLayout);
        }
    }
首先,我们先通过removeAllViews来清空布局,以免发生什么可怕的事情,然后通过循环adapter中的getparentCount方法来加载总共有几项父菜单,然后我们通过innerLinearLayout来add子菜单的布局,方便接下来我们对子菜单的高度的计算。然后我们通过对parentView和childView设置tag来实现点击事件,其中的tagToInt的方法在这里。

 private int TagToInt(String str,int start,int end){
        String substring = str.substring(start, end);
        int i= Integer.parseInt(substring);
        return i;
    }
我们在adapter中通过接口onDataChanged 实现了notify的功能,所以我们需要在SettingView中对它进行实现,所以在settingView中实现该接口,并完成回调。
    @Override
    public void changed() {
        changeAdapter();
    }
以及我们对click事件的回调。

   public  OnItemClickListener onItemClickListener;

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface  OnItemClickListener{
        void ParentClicked(int position);
        void childClicked(int parentPosition,int childPosition);
    }

这样的话我们的控件已经有点样子了,但是不满足是程序员进步的最大动力,所以我继续封装了对于控件高度的计算。

 public List<View>  getChildLayout(){
        List<View> list=new ArrayList<>();
        for(int i=1;i<getChildCount();i+=2){
            View childView = getChildAt(i);
            list.add(childView);
        }
        return list;
    }
这里的代码会有点生涩,首先我们通过循环子控件的个数,第0个是父菜单的parentView,第1个才是我们所需要的子菜单的innerlayout,所以我们从1开始循环,i+=2,也就非常容易的就懂了,因为我们的子菜单的view是每隔一行parentView的,所以需要i+=2。这样我们就得了子菜单的所有view集合。接下来我们就开始计算高度,上文中,我们提到了在activity中和fragment中计算高度的方法是不一样的,所以我们在这里定义一个boolean值来判断是否在activity中。

 public   boolean isActivity=true;
然后在activity中通过post方法来获取子菜单View的高度。

this.post(new Runnable() {
                @Override
                public void run() {
                    List<View> childLayout = getChildLayout();
                    heightList.clear();
                    for(int i=0;i<childLayout.size();i++){
                        int height = childLayout.get(i).getHeight();
                        heightList.add(height);
                    }
                }
            });
然后再fragment中就稍微复杂一些,首先我们要实现 ViewTreeObserver.OnGlobalLayoutListener这个监听来监听控件的高度。

ViewTreeObserver.OnGlobalLayoutListener listener=new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            List<View> childLayout = getChildLayout();
            heightList.clear();
            for(int i=0;i<childLayout.size();i++){
                int height = childLayout.get(i).getHeight();
                heightList.add(height);
            }
        }
    };
这样我们不管在activity中还是fragment中都获取了控件的高度,于是我们继续封装出来一个方法来判断是否在activity中。
  public void initHeight(boolean isActivity){
        this.isActivity=isActivity;
        if(isActivity){
            this.post(new Runnable() {
                @Override
                public void run() {
                    List<View> childLayout = getChildLayout();
                    heightList.clear();
                    for(int i=0;i<childLayout.size();i++){
                        int height = childLayout.get(i).getHeight();
                        heightList.add(height);
                    }
                }
            });
        }else{
            this.getViewTreeObserver().addOnGlobalLayoutListener(listener);
        }
    }
于是我们在每次调用的时候只需要在findview后面initHeight方法来获取子菜单view的高度集合了。值得注意的是因为ViewTreeObserver.OnGlobalLayoutListener是适时监听的,所以我们需要在首次点击之后动画效果生成之前移除他的监听。
  public ViewTreeObserver.OnGlobalLayoutListener getListener(){
        return  listener;
    }

    //移除监听
    public  void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener) {
        if (Build.VERSION.SDK_INT < 16) {
            v.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
        } else {
            v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
        }
    }
这样我们就基本上完成了对控件的封装,最后我们在对上篇博客中的动画效果做最后一层的封装。
<pre name="code" class="java"> public void animateBegin(int position){
        if (getChildLayout().get(position).getVisibility() == View.VISIBLE) {
            if(!isActivity){
                removeOnGlobalLayoutListener(getChildView(position), getListener());
            }
            AnimUtils.animatorClose(getChildLayout().get(position), getChildItemHeight(position));
        } else {
            AnimUtils.animatorOpen(getChildLayout().get(position), getChildItemHeight(position));
        }
    }
这里的封装也十分简单,首先就是判断是否在fragment中,如果是fragment我们就移除掉对于控件高度的监听,是不是非常的通俗易懂呢?最后我们来实践一下。

<pre name="code" class="html"><RelativeLayout 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">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >
    <com.example.mydemo2.SettingsView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/sv"
        ></com.example.mydemo2.SettingsView>

    <Button
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:id="@+id/change_fragment"
        android:text="切换到fragment"
        />
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:id="@+id/fragment_layout"
        ></FrameLayout>
        </LinearLayout>
    </ScrollView>
</RelativeLayout>


这是MainActivity的布局文件,我们点击button之后会调用repalce fragment的方法,来显示fragment中的settingview。

 sv= (SettingsView) findViewById(R.id.sv);
        btn= (Button) findViewById(R.id.change_fragment);
        sv.initHeight(true);
        for(int i=0;i<3;i++){
            mList.add(i+"这是父菜单");
        }
        for(int i=0;i<3;i++){
            List<String> list=new ArrayList<>();
            for(int j=0;j<5;j++){
                list.add("这是第"+i+"父菜单,第+"+j+"子菜单");
            }
            mList2.add(list);
        }
        SettingsAdapter adapter=new SettingsAdapter(MainActivity.this,mList,mList2) {
            @Override
            public View getParentView(View view, int position) {
                View view1 = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_a, null);
                TextView tv= (TextView) view1.findViewById(R.id.tv1);
                tv.setText(mList.get(position));
                return view1;
            }

            @Override
            public View getChildView(View view, int parentPosition, int childPosition) {
                View view2 = LayoutInflater.from(MainActivity.this).inflate(R.layout.item_b, null);
                TextView tv= (TextView) view2.findViewById(R.id.tv2);
                tv.setText(mList2.get(parentPosition).get(childPosition));
                return view2;
            }
        };
        sv.setAdapter(adapter);
        sv.setOnItemClickListener(new SettingsView.OnItemClickListener() {
            @Override
            public void ParentClicked(int position) {
                sv.animateBegin(position);
            }

            @Override
            public void childClicked(int parentPosition, int childPosition) {
                Toast.makeText(MainActivity.this, mList2.get(parentPosition).get(childPosition), Toast.LENGTH_SHORT).show();
            }
        });


        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
                TestFragment fragment=new TestFragment();
                ft.replace(R.id.fragment_layout,fragment);
                ft.commit();
            }
        });
    }
这是MainActivity中全部的代码,并没有什么非常有难度的,在我们的封装下变得十分的简单并且清晰,即使你以后甩锅不干之后,也能造福下一个程序汪,是不是很好呢?汪汪汪!最后附上效果图一张:


















评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值