效果
一 布局分析:
分成三部分
1.菜单栏TabView部分 本文采用线性布局包裹TextView的形式 采用LinearLayout的原因是每个Tab页可以使用权重做到均分LinearLayout的效果
2.主题内容MainContent部分 本文为了简单处理 直接放一个相对布局
3.阴影部分ShadowView 是个半透明的View
3个部分放在一个相对布局中
二 添加适配器模式并进行查漏补缺
这里的适配器为了数据和Custom FilterView能够进行适配,使用不同的Adapter可以显示不同的数据 或者如果Adapter做的强大,可以使用一个Adapter显示各种格式的数据(本文以String数组为例)
具体的适配器模式可以参考这篇文章
https://blog.csdn.net/u011109881/article/details/82288922
给tabView的每个item增加点击动作
轮巡tab页 看看是不是点击了tab页
如果点击了当前显示的tab页 关闭主体内容 切换tab页颜色
如果点击了非当前显示的tab页 tab页的颜色需要变化 内容需要变化
给阴影增加点击动作关闭菜单
三 添加动画
动画没有什么难度 调试几把就可以了 主要这里练习了AnimatorSet可以进行组合动画
四 观察者模式练习
具体的观察者模式可以参考这篇文章
https://blog.csdn.net/u011109881/article/details/81043699
需求:
一般情况下我们的内容页主要是一些列表 那么我们希望点击列表页面的时候 能够获取到当前点击的item项 并同时关闭filteView界面(调用closeContent方法),那么我们势必要在adapter调用closeContent方法 观察者模式就是为了实现这个功能而产生的。(虽然说adapter直接持有CustomFilterView对象也可以实现 但是这样耦合性提高)
具体做法可以参考ListView setAdapter的相关写法
我将ListView 和当前的Demo相似的类列出如下
观察者模式的工作过程:
CustomFilterView包含一个观察者AdapterObserver,在setAdapter的时候注册到事件源/被观察者上,AdapterObserver成为实际观察者
当事件源发生事件事 调用notifyDataSetChanged 通知每一个观察者,因为CustomFilterView持有AdapterObserver,所以能够收到通知
正如我在https://blog.csdn.net/u011109881/article/details/81043699中所描述的一样
其实Java API的设计有点问题,在Java编程经验中有一条:要面向接口编程而不是面向实现。
我们发现原先我们自己写的Subject是个接口,而JAVA
API中的Observable是个实体类,这样如果我们的被观察者继承了Observable就无法继承其他类了,这就限制了被观察者实现的实体类的扩展和复用。
另外Java编程提倡多用组合少用继承。但是Observable将changed的控制方法大多设置为protected,这就意味着,即使我们把Observable的实体类组合到Observer实体类中,也无法控制changed因为protected只在本类及其子类可用。这违反了用组合少用继承,使得程序不够灵活,有时会限制程序的扩展。
因此上面的文件我尽量声明为接口 这是与视频以及源码不同之处
五 部分代码
Activity
public class MainActivity extends AppCompatActivity {
CustomFilterView mCustomFilterView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewAdapter filterViewAdapter = new ViewBaseAdapter(this);
mCustomFilterView = findViewById(R.id.filterView);
mCustomFilterView.setFilterViewAdapter(filterViewAdapter);
}
}
观察者接口
interface Observer {//角色定位 抽象观察者 类比Observer
void notifyDataSetChanged();
}
View
class CustomFilterView extends RelativeLayout implements View.OnClickListener {//角色定位 CustomFilterView相当于ListView
private static final long ANIMATION_DURATION = 300;
private static final String TAG = "CustomFilterView";
private LinearLayout mContainerTab;
private RelativeLayout mContainerContent;
private View mShadowView;
private ViewAdapter mFilterViewAdapter;
private int mCurrentTabIndex = 0;//当前所处Tab的位置
private boolean isAnimating = false;
private AdapterObserver mObserver;
public CustomFilterView(Context context) {
this(context, null);
}
public CustomFilterView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomFilterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//还是感觉使用xml创建的方式更加清晰 使用代码添加布局适合用在添加一两个View的情况 一旦View的个数增加 逻辑也变得复杂,层次不够清晰 容易出bug
inflate(getContext(), R.layout.layout_filter, this);
mContainerTab = findViewById(R.id.container_tab);
mContainerContent = findViewById(R.id.container_content);
mShadowView = findViewById(R.id.view_shadow);
}
public void setFilterViewAdapter(ViewAdapter filterViewAdapter) {
//参考ListView的setAdapter
if (mFilterViewAdapter != null && mObserver != null) {
mFilterViewAdapter.unregisterObserver(mObserver);
}
this.mFilterViewAdapter = filterViewAdapter;
mObserver = new AdapterObserver();
//Adapter是具体的观察者(调用registerObserver的对象是观察者)
mFilterViewAdapter.registerObserver(mObserver);
//获取当前有几个tab页
int count = mFilterViewAdapter.getCount();
for (int i = 0; i < count; i++) {
//显示tabView部分
TextView tabView = (TextView) mFilterViewAdapter.getTabView(i);
LinearLayout.LayoutParams tabViewLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
tabViewLayoutParams.weight = 1;
tabView.setLayoutParams(tabViewLayoutParams);
tabView.setTextColor(Color.BLACK);
mContainerTab.addView(tabView);
//给tabView的每个item增加点击动作
tabView.setOnClickListener(this);
//显示content部分 只显示第一页
if (i == 0) {
View contentView = mFilterViewAdapter.getContentView(i);
mContainerContent.addView(contentView);
}
}
//给第零页设置为选中状态
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setTextColor(Color.RED);
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setBackgroundColor(Color.GRAY);
mShadowView.setOnClickListener(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//设置阴影区域为整个view的1/5
LayoutParams mShadowViewLayoutParams = (LayoutParams) mShadowView.getLayoutParams();
mShadowViewLayoutParams.height = MeasureSpec.getSize(heightMeasureSpec) / 5;
mShadowView.setLayoutParams(mShadowViewLayoutParams);
}
@Override
public void onClick(View v) {
//轮巡tab页 看看是不是点击了tab页
int tabCount = mFilterViewAdapter.getCount();
for (int i = 0; i < tabCount; i++) {
Log.d(TAG, "onClick: " + isAnimating);
//点击了某一个tab页
if (mContainerTab.getChildAt(i) == v) {
//点击了当前显示的tab页 关闭主体内容 切换tab页颜色
if (mContainerTab.getChildAt(mCurrentTabIndex) == v) {
closeContent();
} else if (mCurrentTabIndex == -1) {//当前没有显示的tab页
openContent(i);
} else {//点击了非当前显示的tab页 tab页text的颜色需要变化(注意新旧的当前页面都要变化) 内容需要变化
//旧tab状态更新
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setTextColor(Color.BLACK);
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setBackgroundColor(Color.WHITE);
mCurrentTabIndex = i;
//新tab页状态更新
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setTextColor(Color.RED);
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setBackgroundColor(Color.GRAY);
mContainerContent.removeAllViews();
mContainerContent.addView(mFilterViewAdapter.getContentView(i));
}
}
}
//如果点击了shadowView 同样关闭主体部分
if (v == mShadowView) {
closeContent();
}
}
private void closeContent() {
if (isAnimating) {
return;
}
isAnimating = true;
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setTextColor(Color.BLACK);
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setBackgroundColor(Color.WHITE);
mCurrentTabIndex = -1;
//添加旧页面关闭动画
ObjectAnimator transactionAnimateInY = ObjectAnimator.ofFloat(mContainerContent, "translationY", 0, -mContainerContent.getMeasuredHeight());
ObjectAnimator alphaAnimate = ObjectAnimator.ofFloat(mShadowView, "alpha", 1f, 0);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(ANIMATION_DURATION);
//顺序播放动画
animatorSet.playTogether(transactionAnimateInY, alphaAnimate);
animatorSet.start();
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mContainerContent.setVisibility(GONE);
mShadowView.setVisibility(GONE);
isAnimating = false;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
private void openContent(int tabIndex) {
if (isAnimating) {
return;
}
isAnimating = true;
//新tab页状态更新
mCurrentTabIndex = tabIndex;
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setTextColor(Color.RED);
((TextView) mContainerTab.getChildAt(mCurrentTabIndex)).setBackgroundColor(Color.GRAY);
mContainerContent.setVisibility(VISIBLE);
mContainerContent.removeAllViews();
mContainerContent.addView(mFilterViewAdapter.getContentView(tabIndex));
//添加新页面打开动画
//设置主体位置的位移动画和阴影透明度动画
mShadowView.setVisibility(VISIBLE);
ObjectAnimator transactionAnimateInY = ObjectAnimator.ofFloat(mContainerContent, "translationY", -mContainerContent.getMeasuredHeight(), 0);
ObjectAnimator alphaAnimate = ObjectAnimator.ofFloat(mShadowView, "alpha", 0, 1f);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(ANIMATION_DURATION);
//顺序播放动画
animatorSet.playTogether(transactionAnimateInY, alphaAnimate);
animatorSet.start();
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
//角色定位 具体观察者 类比ListView中的AdapterDataSetObserver
class AdapterObserver implements Observer {
@Override
public void notifyDataSetChanged() {
closeContent();
}
}
}
Adapter接口
public interface ViewAdapter {
//角色定位 抽象的被观察者 参考ListView的Adapter.java
void registerObserver(Observer observer);
void unregisterObserver(Observer observer);
void notifyContentItemClick(View view);
// 获取总共有几个tab页
abstract int getCount();
// 获取当前tab的TabView
abstract View getTabView(int position);
// 获取当前tab的菜单内容
abstract View getContentView(int position);
}
Adapter实现类
class ViewBaseAdapter implements ViewAdapter {//角色定位 具体的被观察者 类比ListView的BaseAdapter
private Context mContext;
String[] tabTexts = {"美食", "生活", "鬼畜", "动漫区", "其他"};
protected final ArrayList<Observer> mObservers = new ArrayList<>();
@Override
public void registerObserver(Observer observer) {
mObservers.add(observer);
}
@Override
public void unregisterObserver(Observer observer) {
mObservers.remove(observer);
}
@Override
public void notifyContentItemClick(View view) {
for (Observer observer : mObservers) {
observer.notifyDataSetChanged();
}
}
public ViewBaseAdapter(Context context) {
this.mContext = context;
}
@Override
public int getCount() {
return tabTexts.length;
}
@Override
public View getTabView(int position) {
TextView textTabView = new TextView(mContext);
textTabView.setPadding(10, 10, 10, 10);
textTabView.setGravity(Gravity.CENTER);
textTabView.setText(tabTexts[position]);
return textTabView;
}
@Override
public View getContentView(int position) {
TextView textContentView = new TextView(mContext);
textContentView.setGravity(Gravity.CENTER);
textContentView.setText(tabTexts[position]);
RelativeLayout.LayoutParams contentViewLayoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
contentViewLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
textContentView.setLayoutParams(contentViewLayoutParams);
textContentView.setOnClickListener(v -> {
//事件源 具体的被观察者 这里通知其他观察者事件
notifyContentItemClick(v);
});
return textContentView;
}
}
XML
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="Hello World!" />
<com.example.customfilterview.CustomFilterView
android:id="@+id/filterView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:background="#cfc"
android:layout_below="@+id/container_tab"
android:layout_above="@+id/view_shadow"
android:id="@+id/container_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
<LinearLayout
android:background="#fff"
android:id="@+id/container_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />
<View
android:layout_alignParentBottom="true"
android:id="@+id/view_shadow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#88888888" />
</RelativeLayout>
后记
后来在准备文章的时候测试到一些bug,当时已经很晚了,虽然有点累了 还是想在当天fix,然而花了将近1个多小时还是没有理清头绪。后来实在没有办法,只得留给第二天解决。早上看了不到五分钟就定位并解决问题了。人的大脑真实神奇,要高效的完成一项任务,充分的休息和清醒的大脑是必要的,否则只是堆砌事件反而没什么效果。
代码:
https://github.com/caihuijian/learn_darren_android