最近UI 设计了一个小动画,作为Android 小程序员,自然不能示弱,在不屑的努力下,终于做出了一个Demo的雏形。
完成效果如下
准备工作
首先添加要使用的控件
dependencies {
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'com.android.support:design:25.3.1'
}
新建一个自定义 ScrollView 类
public class DetailScrollView extends ScrollView
{
public DetailScrollView(Context context)
{
this(context, null);
}
public DetailScrollView(Context context, AttributeSet attrs)
{
super(context, attrs);
}
}
布局文件大致写一下
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:src="@android:drawable/ic_menu_sort_by_size" />
<ImageView
android:id="@+id/iv_detail_collect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_margin="12dp"
android:src="@android:drawable/btn_star_big_off" />
<!--下面开始就是猪脚了-->
<next.vr_view2.view.DetailScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="200dp"
android:layout_height="234dp"
android:layout_gravity="center_horizontal">
<ImageView
android:id="@+id/iv_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:src="@mipmap/ic_launcher_round" />
</RelativeLayout>
<android.support.design.widget.TabLayout
android:id="@+id/tl_detail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorColor="#ee1717"
app:tabSelectedTextColor="#ee1717"
app:tabTextColor="#515151">
</android.support.design.widget.TabLayout>
<FrameLayout
android:id="@+id/fl_fragment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#f4f4f4" />
</LinearLayout>
</next.vr_view2.view.DetailScrollView>
</FrameLayout>
Activity 也简单的添两句
public class DetailActivity extends AppCompatActivity implements TabLayout.OnTabSelectedListener
{
TabLayout mTabLayout;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.f_shop_detail_view);
mTabLayout = (TabLayout) findViewById(R.id.tl_detail);
mTabLayout.addTab(mTabLayout.newTab().setText("Tab1"));
mTabLayout.addTab(mTabLayout.newTab().setText("Tab2"));
mTabLayout.addOnTabSelectedListener(this);
}
@Override
public void onTabSelected(TabLayout.Tab tab)
{
}
}
对View 进行初始化
回到 咋们的自定义View 中来
首先通过onSizeChanged
获取这个View 可用的总高度
//这个View 会使用的高度
private int thisViewHeight;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
//首先在 获取这个View 的高度
thisViewHeight = h;
}
然后获取 需要动态改变的ImageView ,这里我偷了一个懒- -
private ImageView mLogoIv;
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
mLogoIv = (ImageView) findViewById(R.id.iv_logo);
}
然后在 onLayout
对布局信息进行初始化
//是否对布局进行初始
private boolean isInitLayout = true;
//滑到顶部超出屏幕高度
private int mScrollTopBeyondScreenHeight;
//将View 高度均分 中的一份
private float ah;
//对 logo ImageView 初始值进行存储
private int logoStartMarginTop;
private int slogoStartWidth;
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
if (isInitLayout)
{
//其实就是布局文件中 我们写的那个LinearLayout
ViewGroup group = (ViewGroup) getChildAt(0);
//RelativeLayout
ViewGroup v1 = (ViewGroup) group.getChildAt(0);
//android.support.design.widget.TabLayout
View v2 = group.getChildAt(1);
//FrameLayout
View v3 = group.getChildAt(2);
// TabLayout 是不会 改变了
// 减去它之后就是我们要使用的,可以改变的长度
int mh = thisViewHeight - v2.getHeight();
//假如我们把总长分为 13份,头部(RelativeLayout)占据 4份
//那尾部(FrameLayout) 就是 13 - 4 份
int maxT = 13;
int topT = 4;
//计算一份的值
ah = mh * 1f / 10;
ViewGroup.LayoutParams v1lp = v1.getLayoutParams();
//+1 是因为 float 转换为 int 会有误差 ,使用+1 来弥补误差
v1lp.height = (int) (ah * topT) + 1;
v1.setLayoutParams(v1lp);
ViewGroup.LayoutParams v3lp = v3.getLayoutParams();
v3lp.height = (int) (ah * (maxT - topT)) + 1;
v3.setLayoutParams(v3lp);
//对要动态改变的ImageView 设置大小,上边距 等。这里的信息可以根据自己的爱好修改。
RelativeLayout.LayoutParams logoLp = (RelativeLayout.LayoutParams) mLogoIv.getLayoutParams();
//对 logo 初始值进行存储
logoLp.topMargin = logoStartMarginTop = (int) (ah / 4);
logoLp.width = slogoStartWidth = v1lp.height / 4;
logoLp.height = slogoStartWidth;
mLogoIv.setLayoutParams(logoLp);
//计算出滑动顶部超出屏幕的高度,这个值很重要
mScrollTopBeyondScreenHeight = v1lp.height + v3lp.height - mh;
//完成初始化
isInitLayout = false;
}
}
我知道这段代码又臭又长,但是一定要冷静看,代码本身并没有难度,这样才能进行下一步~
可以看到大体模样是出来了~
动态改变
计算 需要使用 到 onScrollChanged(int l, int t, int oldl, int oldt)
方法
其中 t 代表是 滑动后 超出屏幕的多少值
语言表达有障碍,所以直接上代码和效果吧
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
{
super.onScrollChanged(l, t, oldl, oldt);
float f = 1 - (t * 1f / mScrollTopBeyondScreenHeight);
Log.i("next", "渐变比例-->" + f);
RelativeLayout.LayoutParams logoLp = (RelativeLayout.LayoutParams) mLogoIv.getLayoutParams();
logoLp.topMargin = logoStartMarginTop + t;
logoLp.width = (int) (slogoStartWidth / 2 + slogoStartWidth / 2 * f);
logoLp.height = logoLp.width;
mLogoIv.setLayoutParams(logoLp);
//TODO 如果还有其他View 需要动态改变可以根据 f 动态进行改变
}
到这里基本效果就算完成了,然后剩下的就是一些意想不到的 意外
当嵌套RecyclerView后的滑动事件处理
为 Activity中添加 RecyclerView ,仅贴出添加代码
private final int LAYOUT_ID = R.id.fl_fragment_container;
private TestFragment tab1Fragment;
private TestFragment tab2Fragment;
private void initData()
{
tab1Fragment = new TestFragment();
tab2Fragment = new TestFragment();
List<String> data1 = new ArrayList<>();
List<String> data2 = new ArrayList<>();
for (int i = 'a'; i < 'z'; i++)
{
data1.add("数据1:" + (char) i);
data2.add((char) i + "--》我的是数据2");
}
tab1Fragment.setData(data1);
tab2Fragment.setData(data2);
getSupportFragmentManager().beginTransaction()//
.add(LAYOUT_ID, tab1Fragment, "tab1")//
.add(LAYOUT_ID, tab2Fragment, "tab2")//
.hide(tab2Fragment)//
.commit();
}
@Override
public void onTabSelected(TabLayout.Tab tab)
{
FragmentTransaction tf = getSupportFragmentManager().beginTransaction();
switch (tab.getPosition())
{
case 0:
tf.show(tab1Fragment).hide(tab2Fragment);
break;
default:
case 1:
tf.show(tab2Fragment).hide(tab1Fragment);
break;
}
tf.commit();
}
至于 TestFragment
就是一个简单的RecyclerView的Fragment
public class TestFragment extends Fragment
{
RecyclerView mRecyclerView;
List<String> data = new ArrayList<>();
TestAdapter mAdapter;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.recycler_view, container, false);
}
public void setData(List<String> data)
{
if (data != null)
this.data = data;
if (mAdapter != null)
mAdapter.notifyDataSetChanged();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState)
{
mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
mRecyclerView.setAdapter(new TestAdapter());
}
private class TestAdapter extends RecyclerView.Adapter<TestViewHolder>
{
@Override
public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
return new TestViewHolder(getActivity().getLayoutInflater().inflate(android.R.layout.simple_list_item_1, parent, false));
}
@Override
public void onBindViewHolder(TestViewHolder holder, int position)
{
holder.mTextView.setText(data.get(position));
}
@Override
public int getItemCount()
{
return data.size();
}
}
private class TestViewHolder extends RecyclerView.ViewHolder
{
TextView mTextView;
public TestViewHolder(View itemView)
{
super(itemView);
mTextView = (TextView) itemView.findViewById(android.R.id.text1);
}
}
}
然后问题就出来了,先上图
问题1 ,RecyclerView 会抢占用ScrollView的 向上滑动事件
问题2,点击Tab进行切换的时候,ScrollView 会滑动到顶部
那么一个个问题来解决
RecyclerView 会抢占用ScrollView的 向上滑动事件
这个问题只需要屏蔽事件即可,当Scroll View 没有滑动到顶部,我们拦截子View 向上滑动事件即可
还是直接看代码吧
//是否滑动到顶部
private boolean isScrollTop;
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt)
{
isScrollTop = t == mScrollTopBeyondScreenHeight;
//... 此处省略之前代码
}
//按下的点
private float lastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
boolean isScrollUp = false;
switch (ev.getAction())
{
case MotionEvent.ACTION_DOWN:
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//y 变小说明是在向上滑
isScrollUp = lastY - ev.getY() > 0;
break;
case MotionEvent.ACTION_UP:
lastY = 0;
break;
}
//如果是向上滑动,并且 没有滑动到顶部,对事件进行拦截
if (isScrollUp && !isScrollTop) return true;
return super.onInterceptTouchEvent(ev);
}
可以看见向上滑动事件受到了制裁
点击Tab进行切换的时候,ScrollView 会滑动到顶部
这个问题我并没有绝对的把握能够处理好,还处于研究中。
我发现每次切换Tab的时候,scrollTo
方法会被执行,所以我取了一个巧,当 滑动 y == 超出屏幕最高度 时就屏蔽事件
@Override
public void scrollTo(int x, int y)
{
if (y == mScrollTopBeyondScreenHeight) return;
//Log.i("next", "scrollTo x ->" + x + " y ->" + y);
super.scrollTo(x, y);
}
效果
问题2 的解决方案还有待考证。