效果图
代码
- 自定义控件类
SlideMenu
这里使用属性动画的方式实现滑动效果
package cn.llj.menu.demo;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
/**
* Created by llj on 2019/3/28.
* 通用分级滑动菜单
*/
public class SlideMenu extends RelativeLayout
{
private static final int MAIN_WIDTH_DPI = 150;//dp 左边栏宽度
private static final int DURATION = 200;//ms 滑动时间
private static final String TV_SELECTED_COLOR = "#00d26E";//tv selected color
private static final String TV_NORMAL_COLOR = "#525866";//tv normal color
private static final String ITEM_BG_COLOR = "#F2F6F9";//tv bg color
private static final String TAG = "SlideMenu";
private int mW;
private int mMainWidth;//px
private List<MenuBean> mList = new ArrayList<>();
private int mCurrentIndex;
private String mCurrentRootName;
private MenuBean mCurrentMenu;
private ISlideMenuListener mListener;
private boolean mIsOddCount = true;//标记奇数个使用不同背景色
private String mSelectedColor=TV_SELECTED_COLOR;
public interface ISlideMenuListener
{
/**
* 菜单监听器(动画结束后回调)
*
* @param hasPre 是否可返回
* @param hasNext 是否可前进
* @param currentRootName 当前所属的根菜单名称
* @param selectedMenu 当前选中的菜单
*/
void onAnimFinished(boolean hasPre, boolean hasNext, String currentRootName, MenuBean
selectedMenu);
}
/**
* 设置监听器
*
* @param listener
*/
void setListener(ISlideMenuListener listener)
{
mListener = listener;
}
public SlideMenu(Context context)
{
super(context);
initMainAndSubViews();
}
public SlideMenu(Context context, AttributeSet attrs)
{
super(context, attrs);
initMainAndSubViews();
}
public void setSelectedColor(String selectedColor)
{
this.mSelectedColor = selectedColor;
}
/**
* 销毁菜单
*/
void destroy()
{
mList.clear();
mCurrentIndex = 0;
mCurrentMenu = null;
mCurrentRootName = null;
mListener = null;
removeAllViews();
}
/**
* 初始化菜单数据,只展示第一级菜单数据
*
* @param list
*/
void initCurrentMenus(List<MenuBean> list)
{
if (list == null || list.size() == 0)
{
Log.d(TAG, "没有数据");
return;
}
mList.clear();
mList.addAll(list);
mCurrentIndex = 0;
mCurrentRootName = mList.get(mCurrentIndex).getName();
mCurrentMenu = mList.get(mCurrentIndex);
MenuAdapter adapter = new MenuAdapter(mList);
// adapter.mSelectedPos = 0;
getChildAt(mCurrentIndex).setAdapter(adapter);
// getChildAt(mCurrentIndex + 1).setAdapter(new MenuAdapter(mList.get(0).getSubItems()));
}
@Override
public ListView getChildAt(int index)
{
if (index < 0 || index >= getChildCount())
{
return null;
}
else return (ListView) super.getChildAt(index);
}
private void initMainAndSubViews()
{
mW = getResources().getDisplayMetrics().widthPixels;
mMainWidth = DeviceUtils.dip2px(getContext(), MAIN_WIDTH_DPI);
//初始化两个子view
ListView mainView = createView();
LayoutParams p = (LayoutParams) mainView.getLayoutParams();
p.width = mMainWidth;
mainView.setOnItemClickListener(mMainClickItemListener);
ListView subView = createView();
subView.setOnItemClickListener(mSubClickItemListener);
p = (LayoutParams) subView.getLayoutParams();
p.leftMargin = mMainWidth;
addView(mainView);
addView(subView);
}
private ListView createView()
{
LayoutParams p = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup
.LayoutParams.MATCH_PARENT);
ListView view = (ListView) View.inflate(getContext(), R.layout.slide_menu_catogery_item,
null);
view.setBackgroundColor(mIsOddCount ? Color.parseColor(ITEM_BG_COLOR) : Color.WHITE);
mIsOddCount = !mIsOddCount;
view.setLayoutParams(p);
return view;
}
//左边菜单项点击监听器
AdapterView.OnItemClickListener mMainClickItemListener = new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (mCurrentIndex == 0)
{
mCurrentRootName = mList.get(position).getName();
}
processSelected(parent, position);
List<MenuBean> list = mCurrentMenu.getSubItems();
if (getChildAt(mCurrentIndex + 1) != null)
getChildAt(mCurrentIndex + 1).setAdapter(new MenuAdapter(list));
if (mListener != null)
{
mListener.onAnimFinished(mCurrentIndex > 0, list.size() > 0, mCurrentRootName,
mCurrentMenu);
}
}
};
//右边菜单项点击监听器
AdapterView.OnItemClickListener mSubClickItemListener = new AdapterView.OnItemClickListener()
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
processSelected(parent, position);
List<MenuBean> list = mCurrentMenu.getSubItems();
if (list.size() > 0)
{//还有子元素,则移动过去
if (getChildAt(mCurrentIndex + 2) == null) //复用已有的view
addView(createView());
//刷新该view的数据
getChildAt(mCurrentIndex + 2).setAdapter(new MenuAdapter(list));
animateForward(list.size() > 0, mCurrentMenu);
}
else
{
if (mListener != null)
{
mListener.onAnimFinished(mCurrentIndex > 0, false, mCurrentRootName,
mCurrentMenu);
}
}
}
};
//处理选中项
private void processSelected(AdapterView<?> parent, int position)
{
MenuAdapter adapter = (MenuAdapter) parent.getAdapter();
adapter.mSelectedPos = position;
adapter.notifyDataSetChanged();
mCurrentMenu = adapter.getItem(position);
}
//处理前进
private void animateForward(final boolean hasNext, final MenuBean currentSelected)
{
setLayoutParams(getChildAt(mCurrentIndex + 1), getChildAt(mCurrentIndex + 2));
ObjectAnimator animator1 = ObjectAnimator.ofFloat(getChildAt(mCurrentIndex),
"translationX", calculateMainViewDistance().first, calculateMainViewDistance().second);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(getChildAt(mCurrentIndex + 1),
"translationX", calculateSubViewDistance().first, calculateSubViewDistance().second);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(getChildAt(mCurrentIndex + 2),
"translationX", mW, mMainWidth);
AnimatorSet set = new AnimatorSet();
set.playTogether(animator1, animator2, animator3);
set.setDuration(DURATION);
set.start();
set.addListener(new AnimatorListenerAdapter()
{
@Override
public void onAnimationEnd(Animator animation)
{
//切换监听对象
getChildAt(mCurrentIndex + 1).setOnItemClickListener(mMainClickItemListener);
//if (getChildAt(mCurrentIndex + 2) != null)
getChildAt(mCurrentIndex + 2).setOnItemClickListener(mSubClickItemListener);
mCurrentIndex++;
if (mListener != null)
{
mListener.onAnimFinished(true, hasNext, mCurrentRootName, currentSelected);
}
}
});
}
/**
* 返回
*/
public void animateBack()
{
if (mCurrentIndex == 0)
{
Log.v(TAG, "当前在第一级,不能回退了");
return;
}
mCurrentIndex--;
setLayoutParams(getChildAt(mCurrentIndex), getChildAt(mCurrentIndex + 1));
ObjectAnimator animator1 = ObjectAnimator.ofFloat(getChildAt(mCurrentIndex),
"translationX", calculateMainViewDistance().second, calculateMainViewDistance().first);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(getChildAt(mCurrentIndex + 1),
"translationX", calculateSubViewDistance().second, calculateSubViewDistance().first);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(getChildAt(mCurrentIndex + 2),
"translationX", mMainWidth, mW);
AnimatorSet set = new AnimatorSet();
set.setDuration(DURATION);
set.playTogether(animator1, animator2, animator3);
set.start();
set.addListener(new AnimatorListenerAdapter()
{
@Override
public void onAnimationEnd(Animator animation)
{
//切换监听对象
getChildAt(mCurrentIndex).setOnItemClickListener(mMainClickItemListener);
getChildAt(mCurrentIndex + 1).setOnItemClickListener(mSubClickItemListener);
if (mListener != null)
{
mListener.onAnimFinished(mCurrentIndex > 0, true, mCurrentRootName,
mCurrentMenu);
}
}
});
}
//右边菜单移到左边后宽度要变化
private void setLayoutParams(ListView mainView, ListView subView)
{
LayoutParams p = (LayoutParams) mainView.getLayoutParams();
p.width = mMainWidth;
mainView.setLayoutParams(p);
p = (LayoutParams) subView.getLayoutParams();
p.width = mW - mMainWidth;
subView.setLayoutParams(p);
}
//计算左边菜单项的动画移动距离
private Pair<Integer, Integer> calculateMainViewDistance()
{
if (mCurrentIndex == 1)
{
return new Pair<>(-mMainWidth, -2 * mMainWidth);
}
else
{
return new Pair<>(0, -mMainWidth);
}
}
//计算右边菜单项的动画移动距离
private Pair<Integer, Integer> calculateSubViewDistance()
{
if (mCurrentIndex == 0)
{
return new Pair<>(0, -mMainWidth);
}
else
{
return new Pair<>(mMainWidth, 0);
}
}
private class MenuAdapter extends BaseAdapter
{
private List<MenuBean> mList = new ArrayList<>();
private int mSelectedPos = -1;
MenuAdapter(@NonNull List<MenuBean> list)
{
mList.clear();
mList.addAll(list);
}
@Override
public int getCount()
{
return mList.size();
}
@Override
public MenuBean getItem(int position)
{
return mList.get(position);
}
@Override
public long getItemId(int position)
{
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
Holder holder = new Holder();
if (convertView == null)
{
convertView = View.inflate(parent.getContext(), R.layout.slide_menu_item, null);
holder.tv = (TextView) convertView;
convertView.setTag(holder);
}
else
{
holder = (Holder) convertView.getTag();
}
Drawable background = parent.getBackground();//// TODO: 待优化
ColorDrawable colorDrawable = (ColorDrawable) background;
int parentColor = colorDrawable.getColor();
if (position == mSelectedPos)
{
holder.tv.setTextColor(Color.parseColor(mSelectedColor));
if (parentColor == Color.WHITE)
holder.tv.setBackgroundColor(Color.parseColor(ITEM_BG_COLOR));
}
else
{
holder.tv.setTextColor(Color.parseColor(TV_NORMAL_COLOR));
if (parentColor == Color.WHITE) holder.tv.setBackgroundColor(Color.WHITE);
}
holder.tv.setText(mList.get(position).getName());
return convertView;
}
}
private class Holder
{
TextView tv;
}
//动画监听器适配器
private abstract class AnimatorListenerAdapter implements Animator.AnimatorListener
{
@Override
public void onAnimationStart(Animator animation)
{
}
@Override
public void onAnimationEnd(Animator animation)
{
}
@Override
public void onAnimationCancel(Animator animation)
{
}
@Override
public void onAnimationRepeat(Animator animation)
{
}
}
}
- 菜单实体类
MenuBean.java
package cn.llj.menu.demo;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
/**
* 滑动菜单项实体类
* Created by llj on 2019/3/28.
*/
public class MenuBean {
private String id;
private String name;
@SerializedName("sub_items")
private List<MenuBean> subItems=new ArrayList<>();
public static List<MenuBean> parse(JsonArray jsonArray){
List<MenuBean> list=new ArrayList<>();
Gson gson = new Gson();
for (JsonElement element:jsonArray){
MenuBean cse = gson.fromJson( element , MenuBean.class);
list.add(cse);
}
return list;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setSubItems(List<MenuBean> subItems) {
this.subItems = subItems;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public List<MenuBean> getSubItems() {
return subItems;
}
}
- 菜单布局文件
slide_menu_item.xml
<?xml version="1.0" encoding="utf-8"?>
<ListView 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"
android:divider="#E3E9ED"
android:dividerHeight="0.5dp"
android:scrollbars="none"
tools:showIn="@layout/activity_main" />
- 菜单项item
slide_menu_item.xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="业务业务知识业务知识知识"
android:ellipsize="end"
android:maxLines="1"
android:padding="20dp"
android:textSize="16sp"
>
</TextView>
- 调用测试类
MainActivity.java
package cn.llj.menu.demo;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import java.util.List;
public class MainActivity extends Activity implements View.OnClickListener
{
private String TAG = "LLJ";
private SlideMenu mSlideMenu;
private View mBackView, mConfirmView;
private String mCurrentName;
String testData = "[ {\"id\": 1, \"name\": \"第一级\", \"sub_items\": [ { \"id\": 2, " +
"\"name\": \"第二级的名称有点惆怅怎么办\", \"sub_items\": [ { \"id\": 3, \"name\": \"第三极\", " +
"\"sub_items\": [ { \"id\": 4, \"name\": \"第四级\", \"sub_items\": [ { \"id\": 5, " +
"\"name\": \"第五级\"} ] } ]} ] } ,{ \"id\": 2, \"name\": \"第二级2\", \"sub_items\": [ {" +
" \"id\": 3, \"name\": \"第三极2\", \"sub_items\": [ { \"id\": 4, \"name\": " +
"\"第四级2\", \"sub_items\": [ { \"id\": 5, \"name\": \"第五级2\"} ] } ]} ] },{ \"id\": 5," +
" \"name\": \"这级没有下一级了\"},{ \"id\": 5, \"name\": \"这级没有下一级了\"},{ \"id\": 5, \"name\":" +
" \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"},{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ] }, {\"id\": 1, \"name\": \"第1级的地方地方斯蒂芬\", \"sub_items\": [ { " +
"\"id\": 2, \"name\": \"第二级\", \"sub_items\": [ { \"id\": 3, \"name\": \"第三极\", " +
"\"sub_items\": [ { \"id\": 4, \"name\": \"第四级\", \"sub_items\": [ { \"id\": 5, " +
"\"name\": \"第五级\"} ] } ]} ] } ,{ \"id\": 2, \"name\": \"第二级2\", \"sub_items\": [ {" +
" \"id\": 3, \"name\": \"第三极2\", \"sub_items\": [ { \"id\": 4, \"name\": " +
"\"第四级2\", \"sub_items\": [ { \"id\": 5, \"name\": \"第五级2\"} ] } ]} ] },{ \"id\": 5," +
" \"name\": \"这级没有下一级了\"},{ \"id\": 5, \"name\": \"这级没有下一级了\"},{ \"id\": 5, \"name\":" +
" \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"},{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ,{ \"id\": 5, \"name\": \"这级没有下一级了\"} ,{ \"id\": 5, \"name\": " +
"\"这级没有下一级了\"} ] }]";
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBackView = findViewById(R.id.back);
mConfirmView = findViewById(R.id.confirm);
mSlideMenu = findViewById(R.id.slideMenu);
JsonParser parser = new JsonParser();
JsonArray Jarray = parser.parse(testData).getAsJsonArray();
List<MenuBean> list = MenuBean.parse(Jarray);
mSlideMenu.initCurrentMenus(list);
mSlideMenu.setListener(new SlideMenu.ISlideMenuListener()
{
@Override
public void onAnimFinished(boolean hasPre, boolean hasNext, String currentRootName,
MenuBean currentMenu)
{
if (hasPre) mBackView.setVisibility(View.VISIBLE);
else mBackView.setVisibility(View.INVISIBLE);
mCurrentName = currentMenu.getName();
Log.d(TAG, "hasNext=" + hasNext + ",rootname=" + currentRootName + "," +
"currentMenu=" + currentMenu.getName());
}
});
mBackView.setOnClickListener(this);
mConfirmView.setOnClickListener(this);
}
@Override
public void onClick(View v)
{
if (v.getId() == R.id.back)
{
mSlideMenu.animateBack();
}
else if (v.getId() == R.id.confirm)
{
Toast.makeText(this.getApplicationContext(), String.format("选中了:%s", mCurrentName),
Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onDestroy()
{
mSlideMenu.destroy();
super.onDestroy();
}
}
MainActivity.java
的布局文件activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
android:background="#205081"
tools:context=".MainActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="400dp">
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:background="@android:color/white"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/back"
android:layout_width="66dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginTop="15dp"
android:orientation="horizontal"
android:visibility="invisible">
<ImageView
android:id="@+id/imageView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="20dp"
android:src="@mipmap/ac_back_icon" />
</LinearLayout>
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:text="知识分类"
android:textColor="#ff323232"
android:textSize="16sp" />
<TextView
android:id="@+id/confirm"
android:layout_width="66dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:gravity="center"
android:text="确定"
android:textColor="#00D26E"
android:textSize="14sp" />
</RelativeLayout>
<cn.llj.menu.demo.SlideMenu
android:id="@+id/slideMenu"
android:layout_width="match_parent"
android:layout_height="match_parent">
</cn.llj.menu.demo.SlideMenu>
</LinearLayout>
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
- gradle依赖
因为我们的实体数据结构是树状结构,android自带的json库和fastjson均不支持这种数据结构的解析,所以我这边使用gson库来解析数据
...
implementation 'com.google.code.gson:gson:2.8.2'
...