转载请注明出处
消息滚动条之所以是自定义ViewGroup(以下使用RotateMsgView)而不是自定义View,是因为不想将每个消息体(View)写死,而是想像ListView一样,内容又每个itemView提供,而RotateMsgView需要做的是将每个View按顺序布局好,并将这些itemView动起来。
这时则需要一个CustomAdapter,和ListView的Adapter的作用是类似的
public interface CustomAdapter {
int getCount();
Object getItem(int position);
View getView(int position, ViewGroup parent);
int getItemViewType(int position);
int getViewTypeCount();
}
设置CustomAdapter的实现类后,将Adapter中每个View添加到RotateMsgView中,而后调用requestLayout(),请求重新调用measure()和layout()
既然借鉴了ListView的Adapter,那么在onMeasure的时候也需要借鉴AbsListView的setItemViewLayoutParams(),为了避免ItemView的根布局设置match_parent在某些时候无效的情况,如以下代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
childHeightSum = 0;// 所有View的高度的总和
for (int i = 0; i < getChildCount(); i++) {
View child = this.getChildAt(i);
setItemViewLayoutParams(child);// 设置每个itemView的LayoutParams
this.measureChild(child, widthMeasureSpec, heightMeasureSpec);
int ch = child.getMeasuredHeight();
childHeightSum += ch;
}
}
/*===借鉴自AbsListView===*/
private void setItemViewLayoutParams(View child) {
final ViewGroup.LayoutParams vlp = child.getLayoutParams();
LayoutParams lp;
if (vlp == null) {
lp = (LayoutParams) generateDefaultLayoutParams();
} else if (!checkLayoutParams(vlp)) {
lp = (LayoutParams) generateLayoutParams(vlp);
} else {
lp = (LayoutParams) vlp;
}
if (lp != vlp) {
child.setLayoutParams(lp);
}
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new RotateMsgView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new RotateMsgView.LayoutParams(getContext(), attrs);
}
public static class LayoutParams extends ViewGroup.LayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
/*===借鉴自AbsListView===*/
measure完毕后,则是将每个itemView放置在合适的位置,在onLayout中的代码如下:
// 此处的firstViewIndex是顶部可见的第一个View的索引值,这里将layout分为两部分,
// 可见View的索引到最后一个View的布局
for (int i = firstViewIndex; i < getChildCount(); i++) {
View child = this.getChildAt(i);
int ch = child.getMeasuredHeight();
child.layout(0, startHeight, child.getMeasuredWidth(), startHeight + ch);
startHeight += ch;
}
// 第1个View到可见View的布局
for (int i = 0; i < firstViewIndex; i++) {
View child = this.getChildAt(i);
int ch = child.getMeasuredHeight();
child.layout(0, startHeight, child.getMeasuredWidth(), startHeight + ch);
startHeight += ch;
}
以上就是从测量到布局所有的View已经完成了,那么就是如何将所有的View动起来了
在gif动图中,滑动完后会有一个暂停的效果,这里是通过ValueAnimator和Runnable完成的,通过postDelayed(),实现滞留效果,通过ValueAnimator来实现上下移动,具体实现看以下代码:
// Runnable中主要就是确定第一个可见View的索引值,然后获取可见View的高度开启ValueAnimator
private void initView(Context context, AttributeSet attrs, int defStyleAttr) {
showAnimation.setDuration(mDelayMillis);
hideAnimation.setDuration(mDelayMillis);
animatorRunnable = new Runnable() {
@Override
public void run() {
if (hasChangeFirstIndexWithMode) {
if (lastMode != mode) {
switch (mode) {
case Mode.MODE_ALPHA:
break;
case Mode.MODE_TRANSLATION_UP:
firstViewIndex++;
break;
case Mode.MODE_TRANSLATION_DOWN:
firstViewIndex--;
break;
}
if (firstViewIndex >= getChildCount()) {
firstViewIndex = 0;
}
if (firstViewIndex < 0) {
firstViewIndex = getChildCount() - 1;
}
}
}
switch (mode) {
case Mode.MODE_ALPHA:
startAnimator(1, 0, duration, mDelayMillis);
break;
case Mode.MODE_TRANSLATION_UP:
startAnimator(0, getChildAt(firstViewIndex).getMeasuredHeight(), duration, mDelayMillis);
break;
case Mode.MODE_TRANSLATION_DOWN:
startAnimator(getChildAt(firstViewIndex).getMeasuredHeight(), 0, duration, mDelayMillis);
break;
}
lastMode = mode;
}
};
}
//==========================================================================
// 主要就是在ValueAnimator开启的过程中不断的调用requestLayout(),
// 而onLayout中就是结合animatedValue做相应的计算,将itemView放在对应的位置
// 在ValueAnimator运行结束后,做个判断处理,修正firstViewIndex,并再次调用postDelayed(),如此形成一个循环
private void startAnimator(float start, float end, long duration, long delayMillis) {
if (animator == null) {
animator = ValueAnimator.ofFloat(start, end);
animator.setDuration(duration);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//初始化动画执行前的布局
switch (mode) {
case Mode.MODE_ALPHA:
case Mode.MODE_TRANSLATION_UP:
if (lastMode == mode) {
firstViewIndex++;
}
break;
case Mode.MODE_TRANSLATION_DOWN:
if (lastMode == mode) {
firstViewIndex--;
}
break;
}
if (firstViewIndex >= getChildCount()) {
firstViewIndex = 0;
}
if (firstViewIndex < 0) {
firstViewIndex = getChildCount() - 1;
}
postDelayed(animatorRunnable, mDelayMillis);
requestLayout();
Log.v("shan", "=====================================");
}
});
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animatedValue = (Float) animation.getAnimatedValue();
Log.v("shan", "animatedValue:" + animatedValue);
requestLayout();
}
});
animator.start();
} else {//动画取消后再运行,会重置一切animator的参数
if (!animator.isRunning()) {
if (null == propertyValuesHolder) {
propertyValuesHolder = PropertyValuesHolder.ofFloat("y", start, end);
} else {
propertyValuesHolder.setFloatValues(start, end);
}
animator.setValues(propertyValuesHolder);
animator.setDuration(duration);
animator.start();
}
}
}
至此RotateMsgView大致结束了,还有个滞留的时候的渐隐渐显的效果是无关紧要的(RotateMsgView的高度一旦设定小于所有itemView的高度总和),其次就是一些细节处理了,具体看源码
https://github.com/cqf-hn/RotateMsgView