自制控件4——仿名片全能王iOS下拉名片详情
2016-12-08 本文已影响277人
阿敏其人
本文出自 “阿敏其人” 简书博客,转载或引用请注明出处。
iOS下拉名片详情.gif嗯,打开名片全能王扫描名片详情,我们发现iOS和Android的样式是不一样的,简单来说,iOS的好看一些。大概如下图
既然这样,就来简单地仿造一个Android版吧。
先看仿造后的成品效果图:
android.gif大概的功能是出来的,接下来,上代码。
一、自定义拉伸控件
分析:把要自定义的控件分为两个部分,一个是顶部部分,简称为upPart,一个是底部部分,简称为downPart。
upPart 就是上面的那张图片,仅此而已。
downPart 就是下面的灰色覆盖层和很多文字信息。
一、1、准备好的两个布局文件
upPart 布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="160dp">
<ImageView
android:id="@+id/mIvTopPic"
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="#66ff0000"
/>
</LinearLayout>
.
.
.
downPart 布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="#66383636">
<TextView
android:id="@+id/mIvCircle"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/shape_circle_gray_white"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:text="蒙"
android:gravity="center"
android:textSize="20sp"
/>
<TextView
android:id="@+id/mIvDownGrayPic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_centerHorizontal="true"
android:layout_below="@id/mIvCircle"
android:text="蒙奇-D-路飞"
android:gravity="center"
android:textSize="20sp"
android:textColor="#ffffff"
/>
</RelativeLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="TEXT 1"
android:background="#660000FF"
android:layout_margin="10dp"
android:gravity="center"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="TEXT 2"
android:background="#660000FF"
android:layout_margin="10dp"
android:gravity="center"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="TEXT 3"
android:background="#660000FF"
android:layout_margin="10dp"
android:gravity="center"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="TEXT 4"
android:background="#660000FF"
android:layout_margin="10dp"
android:gravity="center"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="TEXT 5"
android:background="#660000FF"
android:layout_margin="10dp"
android:gravity="center"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="TEXT 6"
android:background="#660000FF"
android:layout_margin="10dp"
android:gravity="center"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="TEXT 1"
android:background="#660000FF"
android:layout_margin="10dp"
android:gravity="center"
/>
</LinearLayout>
.
.
.
一、2、自定义控件 StretchView
过程的大概这么走:
1、onMeasure测量一下
2、onLayout布局摆放位置
3、利用onTouchEvent和viewDragHelper实现平滑滚动
主要逻辑就在StretchDraeHelper里面
- A:为了实现类似下拉回弹的效果,我们简单地直接在clampViewPositionVertical里面返回top。不做严格的边界控制。
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//return super.clampViewPositionVertical(child, top, dy);
showTag("clampViewPositionVertical "+ top);
// 颜色的边界控制
/*if(top<0){
return 0;
}else if(top>mUpPartHeight){
return mUpPartHeight;
}*/
return top;
}
- B:正常来说,我们以为upPart的高度作为伸缩/展开的临界判断标准,但是为了向上或者向下的速度足够快的时候,也执行伸缩或者展开,我们另外进行了判断:
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 方法的参数里面没有top,那么我们就采用 getTop()这个方法
int releasePartTop = mDownPart.getTop();
showTag("记录 yvel "+yvel);
float changeStatuValue = 200;
if(yvel>changeStatuValue && isFullStretch == false){ // 关闭状态下,向下的滑动速率足够即使不到一半也展开downPartView
openStretchView();
}else if(yvel<-changeStatuValue && isFullStretch == true){ // 打开状态,向上的滑动速率足够也关闭downPartView
closeStretchView();
}else{ // 普通滑动速率,以为upPart的中间点为临界点
if((mUpPartHeight*0.5)>releasePartTop){
//mDownPart.layout(0,0, mUpPartWidth, mUpPartHeight);
// 利用smoothSlideViewTo 产生平滑过渡的效果 (需要结合invalidate)
closeStretchView();
}else{
//mDownPart.layout(0, mUpPartHeight,mDownPartWidth, mUpPartHeight + mDownPartHeight);
openStretchView();
}
}
invalidate();
super.onViewReleased(releasedChild, xvel, yvel);
}
public class StretchView extends ViewGroup{
public static String TAG = "STRETCH";
private View mUpPart; // 上半部分
private View mDownPart; // 下半部分
private ViewDragHelper viewDragHelper;
private int mUpPartWidth;
private int mUpPartHeight;
private int mDownPartWidth;
private int mDownPartHeight;
private boolean isFullStretch = true;
public StretchView(Context context) {
super(context);
}
public StretchView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public StretchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mUpPart = getChildAt(0);
mDownPart = getChildAt(1);
viewDragHelper = ViewDragHelper.create(this,new StretchDraeHelper());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// measure upPart
LayoutParams upPartLayoutParams = mUpPart.getLayoutParams();
int upPartMeasureHeight = MeasureSpec.makeMeasureSpec(upPartLayoutParams.height,MeasureSpec.EXACTLY);
mUpPart.measure(widthMeasureSpec,upPartMeasureHeight);
// measure downPart
LayoutParams downLayoutParams = mDownPart.getLayoutParams();
int downMeasurePartHeight = MeasureSpec.makeMeasureSpec(downLayoutParams.height,MeasureSpec.EXACTLY);
mDownPart.measure(widthMeasureSpec,downMeasurePartHeight);
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mUpPartWidth = mUpPart.getMeasuredWidth();
mUpPartHeight = mUpPart.getMeasuredHeight();
showTag("mUpPartWidth "+ mUpPartWidth);
showTag("mUpPartHeight "+ mUpPartHeight);
mUpPart.layout(0,0, mUpPartWidth, mUpPartHeight); // 摆放上部分的位置
mDownPartWidth = mDownPart.getMeasuredWidth();
mDownPartHeight = mDownPart.getMeasuredHeight();
showTag("mDownPartWidth "+ mDownPartWidth);
showTag("mDownPartHeight "+ mDownPartHeight);
mDownPart.layout(0, mUpPartHeight,
mDownPartWidth, mUpPartHeight + mDownPartHeight); // 摆放删除部分的位置
}
/**
* ViewDragHelper
*
* 使用ViewDragHelper必须复写onTouchEvent并调用这个方法,才能使touch被消费
*/
class StretchDraeHelper extends ViewDragHelper.Callback{
/**
* Touch的down事件会回调这个方法 tryCaptureView
*
* @Child:指定要动的孩子 (哪个孩子需要动起来)
* @pointerId: 点的标记
* @return : ViewDragHelper是否继续分析处理 child的相关touch事件
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return mDownPart == child;
}
/**
*
* 捕获了水平方向移动的位移数据
* @param child 移动的孩子View
* @param left 父容器的左上角到孩子View的距离
* @param dx 增量值,其实就是移动的孩子View的左上角距离控件(父亲)的距离,包含正负
* @return 如何动
*
* 调用完此方法,在android2.3以上就会动起来了,2.3以及以下是海动不了的
* 2.3不兼容怎么办?没事,我们复写onViewPositionChanged就是为了解决这个问题的
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return super.clampViewPositionHorizontal(child, left, dx);
}
/**
* 捕获了垂直方向移动的位移数据
* @param child
* @param top
* @param dy
* @return
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//return super.clampViewPositionVertical(child, top, dy);
showTag("clampViewPositionVertical "+ top);
// 颜色的边界控制
/*if(top<0){
return 0;
}else if(top>mUpPartHeight){
return mUpPartHeight;
}*/
return top;
}
/**
* 当View的位置改变时的回调
* @param changedView 哪个View的位置改变了
* @param left changedView的left
* @param top changedView的top
* @param dx x方向的上的增量值
* @param dy y方向上的增量值 取值范围为 ±24000 之间 向下为正,向上负
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
invalidate();
showTag("onViewPositionChanged "+ top);
showTag("onViewPositionChanged "+ left);
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 方法的参数里面没有top,那么我们就采用 getTop()这个方法
int releasePartTop = mDownPart.getTop();
showTag("记录 yvel "+yvel);
float changeStatuValue = 200;
if(yvel>changeStatuValue && isFullStretch == false){ // 关闭状态下,向下的滑动速率足够即使不到一半也展开downPartView
openStretchView();
}else if(yvel<-changeStatuValue && isFullStretch == true){ // 打开状态,向上的滑动速率足够也关闭downPartView
closeStretchView();
}else{ // 普通滑动速率,以为upPart的中间点为临界点
if((mUpPartHeight*0.5)>releasePartTop){
//mDownPart.layout(0,0, mUpPartWidth, mUpPartHeight);
// 利用smoothSlideViewTo 产生平滑过渡的效果 (需要结合invalidate)
closeStretchView();
}else{
//mDownPart.layout(0, mUpPartHeight,mDownPartWidth, mUpPartHeight + mDownPartHeight);
openStretchView();
}
}
invalidate();
super.onViewReleased(releasedChild, xvel, yvel);
}
/**
* 整个View拓展起来
*/
private void openStretchView() {
viewDragHelper.smoothSlideViewTo(mDownPart,0,mUpPartHeight);
isFullStretch = true;
}
/**
* 整个view收缩起来
* @return
*/
private void closeStretchView() {
viewDragHelper.smoothSlideViewTo(mDownPart,0,0);
isFullStretch = false;
}
}
@Override
public void computeScroll() {
//super.computeScroll();
// 把捕获的View适当的时间移动,其实也可以理解为 smoothSlideViewTo 的模拟过程还没完成
if(viewDragHelper.continueSettling(true)){
invalidate();
}
// 其实这个动画过渡的过程大概在怎么走呢?
// 1、smoothSlideViewTo方法进行模拟数据,模拟后就就调用invalidate();
// 2、invalidate()最终调用computeScroll,computeScroll做一次细微动画,
// computeScroll判断模拟数据是否彻底完成,还没完成会再次调用invalidate
// 3、递归调用,知道数据noni完成。
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//return super.onTouchEvent(event);
/**Process a touch event received by the parent view. This method will dispatch callback events
as needed before returning. The parent view's onTouchEvent implementation should call this. */
viewDragHelper.processTouchEvent(event); // 使用ViewDragHelper必须复写onTouchEvent并调用这个方法
return true; //消费这个touch
}
private void showTag(String str){
Log.d(TAG,str);
}
}
二、使用 StretchView 控件
布局文件
<?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"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<android.support.v7.widget.Toolbar
android:id="@+id/mToolBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#00000000"
android:layout_marginTop="20dp"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
/>
<com.amqr.likepaperstretch.widget.StretchView
android:id="@+id/mStretchView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/item_stretch_up"/>
<include layout="@layout/item_stretch_down"/>
</com.amqr.likepaperstretch.widget.StretchView>
</RelativeLayout>
需要注意的是,com.amqr.likepaperstretch.widget.StretchView里面的include先后顺序非常重要,不可随意颠倒。
.
.
.
ShowActivity
public class ShowActivity extends AppCompatActivity{
private Toolbar mToolBar;
private StretchView mStretchView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_show);
mToolBar = (Toolbar) findViewById(R.id.mToolBar);
mToolBar.setTitle("Title");
mToolBar.setTitleTextColor(getResources().getColor(R.color.white));
setSupportActionBar(mToolBar);
mToolBar.setOverflowIcon(getResources().getDrawable(R.drawable.icon_menu)); // 指定菜单按钮图标
mToolBar.setNavigationIcon(R.drawable.arrow_left); // 返回箭头
mToolBar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
mToolBar.setOnMenuItemClickListener(onMenuItemClick);
mStretchView = (StretchView)findViewById(R.id.mStretchView);
mStretchView.findViewById(R.id.mIvTopPic).setBackgroundResource(R.drawable.pic_default);
//Toast.makeText(ShowActivity.this,"弹出菜单",Toast.LENGTH_SHORT).show();
//只对api19以上版本有效
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
setTranslucentStatus(true);
}
}
@TargetApi(19)
private void setTranslucentStatus(boolean on) {
Window win = getWindow();
WindowManager.LayoutParams winParams = win.getAttributes();
final int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
if (on) {
winParams.flags |= bits;
} else {
winParams.flags &= ~bits;
}
win.setAttributes(winParams);
}
// 创建关联菜单
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main,menu);
return true;
}
// 菜单的点击回调
private Toolbar.OnMenuItemClickListener onMenuItemClick = new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
String msg = "";
switch (menuItem.getItemId()) {
case R.id.action_ball:
msg += "Click ball";
break;
case R.id.action_tip:
msg += "Click action_tip";
break;
case R.id.action_menu:
msg += "Click setting";
break;
}
if(!msg.equals("")) {
Toast.makeText(ShowActivity.this, msg, Toast.LENGTH_SHORT).show();
}
return true;
}
};
}
为了让Toolbar的菜单栏总是可以点击,所以我们单独放在自定义控件的上方,而不是在做自定义控件的时候就弄在一起。
我们通过这两段代码去掉标题栏,起到沉浸的效果。
//只对api19以上版本有效
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
setTranslucentStatus(true);
}
@TargetApi(19)
private void setTranslucentStatus(boolean on) {
Window win = getWindow();
WindowManager.LayoutParams winParams = win.getAttributes();
final int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
if (on) {
winParams.flags |= bits;
} else {
winParams.flags &= ~bits;
}
win.setAttributes(winParams);
}
大概就是这样,其实还有很多可以改进的空间的,比如弄一个伸缩或者展开实现其他需求的接口,比如提供打开或者关闭的方法,等等。如果需要,可以自行调整。
本篇完。