├── .gitignore ├── README.md ├── build.gradle ├── demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── bleu │ │ └── slidedetailsdemo │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── cn │ │ │ └── bleu │ │ │ └── slidedetailsdemo │ │ │ ├── ISlideCallback.java │ │ │ ├── MainActivity.java │ │ │ └── fragment │ │ │ ├── BaseFragment.java │ │ │ ├── ListFragment.java │ │ │ ├── ScrollViewFragment.java │ │ │ └── ViewPagerFragment.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── layout_list_item.xml │ │ ├── layout_listview.xml │ │ ├── layout_viewpager.xml │ │ ├── scroll_fragment.xml │ │ └── slidedetails_marker_default_layout.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── cn │ └── bleu │ └── slidedetailsdemo │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── bleu │ │ └── widget │ │ └── slidedetails │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── cn │ │ │ └── bleu │ │ │ └── widget │ │ │ └── slidedetails │ │ │ ├── SlideDebug.java │ │ │ └── SlideDetailsLayout.java │ └── res │ │ └── values │ │ ├── attrs.xml │ │ └── strings.xml │ └── test │ └── java │ └── cn │ └── bleu │ └── widget │ └── slidedetails │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | >高仿淘宝、京东商品详情页面的上拉加载图文详情功能。使用扩展ViewGroup实现,对事件冲突已经做了处理,可嵌套ListView、WebView等自由使用。 2 | 3 | ##技术特点 4 | 5 | 1. 完全继承ViewGroup实现的最小功能; 6 | 2. 针对事件冲突、事件消耗进行处理; 7 | 3. 可嵌套ListView、ViewPager、WebView等; 8 | 4. 快速集成。 9 | 10 | ##代码 11 | 12 | 先贴代码,见[Github](https://github.com/cnbleu/SlideDetailsLayout)。 13 | 14 | ##效果图 15 | 16 | ![](http://7xifbq.com1.z0.glb.clouddn.com/Fp1xaC2l40QBC8OKgHJfPt5qtlLs) 17 | 18 | 19 | ##快速使用 20 | 21 | 与一般的组件使用方式类似,直接在xml中导入即可,需要注意的是,`SlideDetailsLayout`仅获取子节点中的前两个View,其中第一个作为Front,即概略视图;第二个作为Behind,即图文详情页面。 22 | 23 | 此处仅列出关键代码,详细代码请参见demo。 24 | 25 | 1. 布局导入 26 | 27 | 文件名称:activity_main.xml 28 | 29 | 30 | 39 | 40 | 44 | 45 | 50 | 51 | 52 | 53 | 配置信息如下: 54 | 55 | 1. duration:动画时长,默认为300ms; 56 | 2. percent:切换的阈值百分比,如0.2表示滑动具体为屏幕高度的20%时切换; 57 | 3. default_panel:默认展示的面板,仅接受两个enum值:front、behind。 58 | 59 | FrameLayout以及WebView可以切换为任何你想要的View类型,当然可能会存在事件冲突,关于事件冲突的解决参考下文。 60 | 61 | 2. 代码调用: 62 | 63 | `SlideDetailsLayout`支持代码动态调用smoothOpen()来开启第二个面板,smoothClose来关闭第二个面板,默认情况,面板是关闭状态。 64 | 65 | 3. 扩展: 66 | 67 | 如果你嵌套的View与`SlideDetailsLayout`有事件冲突,你可以覆写`canChildScrollVertically(direction)`方法来进行拦截处理。direction为负数时表示向下滑动,反之表示向上滑动。 68 | 69 | ##实现思路 70 | 71 | 一个最小的功能集应该包含以下三个方面: 72 | 73 | 1. 包含两个面板,且可以在上下滑动的时候切换; 74 | 2. 嵌套ListView及WebView时可以正常滑动(图文详情部分假设是通过WebView加载H5页面); 75 | 3. 允许通过代码调用切换面板及切换事件通知。 76 | 77 | ###上下滑动 78 | 79 | 1. 通过`onInterceptTouchEvent`进行事件拦截后,在`onTouchEvent`方法中对触摸信息做进一步处理可以实现竖直方向的滑动; 80 | 81 | 2. 如下图: 82 | ![](http://7xifbq.com1.z0.glb.clouddn.com/Fjq_vDJgvi-7nFtRT8mDZRCCREOB) 83 | 84 | Front、Behind面板是收尾相连上下平铺的两个面板,Front面板的高度为Height,`Top`为Front面板的最顶端,也是坐标系中x轴所在的位置,实红线与虚红线之间的部分为屏幕区域。我们要在屏幕区域滑动两个面板只需要改变两个面板在y轴方向的位移(有正负方向)即可。 85 | 86 | 3. 使滑动生效 87 | 88 | 我们知道,自定义布局中有非常重要的两个环节`onMeasure`(测量)和`onLayout`(布局)。测量决定了View的所占的大小,布局决定了View所处的位置。实现滑动的关键思路就在这里,我们在`onLayout`方法中根据通过onInterceptTouchEvent、onTouchEvent得到的滑动信息进行计算而得到布局的位置信息,并把这个位置信息设置到子View上面即可实现滑动。 89 | 90 | 4. 标尺 91 | 92 | 标尺,即:offset,相对于top的位移。Front面板展示时,offset为0,Behind面板展示时,offset为`Height`。此后所有的计算都是相对于该标尺。 93 | 94 | ###事件冲突处理 95 | 96 | 假设父View所在的方向为外,子View所在的方向为内,则:事件的拦截方向为从外向内,事件的消耗方向为从内向外。如果当前View不拦截事件的话,有一定机会可以消耗事件;如果当前View拦截事件的话,则子View原则上不能接受后续事件。我们根据具体的需要来拦截事件或者捡漏掉的事件消耗掉,就能处理事件的冲突了(实际上没有这么简单,以后再进行更详细的描述)。 97 | 98 | ###面板切换及切换事件通知 99 | 100 | 刚才说到,滑动的标尺是Front相对于Top的移动,且所有的位移计算都是基于该标尺。那我们在切换面板时只需要知道对应的offset值即可。当然,更改完offset值之后不要忘记调用`requestLayout()`方法。 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.0.0-alpha7' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "cn.bleu.slidedetailsdemo" 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile project(':library') 26 | } 27 | -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/gordon/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /demo/src/androidTest/java/cn/bleu/slidedetailsdemo/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.slidedetailsdemo; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/src/main/java/cn/bleu/slidedetailsdemo/ISlideCallback.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.slidedetailsdemo; 2 | 3 | /** 4 | * Project: SlideDetailsLayout
5 | * Create Date: 16/1/25
6 | * Author: Gordon
7 | * Description:
8 | */ 9 | public interface ISlideCallback { 10 | 11 | void openDetails(boolean smooth); 12 | 13 | void closeDetails(boolean smooth); 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/main/java/cn/bleu/slidedetailsdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.slidedetailsdemo; 2 | 3 | import android.os.Build; 4 | import android.os.Bundle; 5 | import android.support.v4.app.FragmentManager; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.webkit.WebSettings; 8 | import android.webkit.WebView; 9 | import android.webkit.WebViewClient; 10 | 11 | import cn.bleu.slidedetailsdemo.fragment.ListFragment; 12 | import cn.bleu.widget.slidedetails.SlideDetailsLayout; 13 | 14 | public class MainActivity extends AppCompatActivity implements ISlideCallback { 15 | 16 | private SlideDetailsLayout mSlideDetailsLayout; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | 23 | mSlideDetailsLayout = (SlideDetailsLayout) findViewById(R.id.slidedetails); 24 | 25 | FragmentManager fm = getSupportFragmentManager(); 26 | fm.beginTransaction().replace(R.id.slidedetails_front, new ListFragment()).commit(); 27 | 28 | // final ListView frontListView = (ListView) findViewById(R.id.slidedetails_front); 29 | // frontListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 30 | // @Override 31 | // public void onItemClick(AdapterView parent, View view, int position, long id) { 32 | // Toast.makeText(MainActivity.this, "clicked: " + position, Toast.LENGTH_SHORT).show(); 33 | // } 34 | // }); 35 | // 36 | // List datas = new ArrayList<>(); 37 | // for (int i = 0; i < 50; i++) { 38 | // datas.add("data: " + i); 39 | // } 40 | // 41 | // final View footView = getLayoutInflater() 42 | // .inflate(R.layout.slidedetails_marker_default_layout, null); 43 | // footView.setOnClickListener(new View.OnClickListener() { 44 | // @Override 45 | // public void onClick(View v) { 46 | // slideDetailsLayout.smoothOpen(false); 47 | // } 48 | // }); 49 | // 50 | // frontListView.addFooterView(footView); 51 | // frontListView.setAdapter(new Adapter(datas)); 52 | 53 | final WebView webView = (WebView) findViewById(R.id.slidedetails_behind); 54 | final WebSettings settings = webView.getSettings(); 55 | settings.setJavaScriptEnabled(true); 56 | settings.setSupportZoom(true); 57 | settings.setBuiltInZoomControls(true); 58 | settings.setUseWideViewPort(true); 59 | settings.setDomStorageEnabled(true); 60 | webView.setWebViewClient(new WebViewClient() { 61 | 62 | @Override 63 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 64 | view.loadUrl(url); 65 | return true; 66 | } 67 | }); 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) { 69 | new Object() { 70 | public void setLoadWithOverviewMode(boolean overview) { 71 | settings.setLoadWithOverviewMode(overview); 72 | } 73 | }.setLoadWithOverviewMode(true); 74 | } 75 | 76 | settings.setCacheMode(WebSettings.LOAD_DEFAULT); 77 | 78 | getWindow().getDecorView().post(new Runnable() { 79 | @Override 80 | public void run() { 81 | webView.loadUrl("http://www.cnbleu.com"); 82 | 83 | } 84 | }); 85 | 86 | 87 | // final ListView behindListView = (ListView) findViewById(R.id.slidedetails_behind); 88 | // behindListView.setAdapter(getListAdapter()); 89 | // 90 | // getWindow().getDecorView().postDelayed(new Runnable() { 91 | // @Override 92 | // public void run() { 93 | // slideDetailsLayout.smoothOpen(true); 94 | // } 95 | // }, 500); 96 | } 97 | 98 | @Override 99 | public void openDetails(boolean smooth) { 100 | mSlideDetailsLayout.smoothOpen(smooth); 101 | } 102 | 103 | @Override 104 | public void closeDetails(boolean smooth) { 105 | mSlideDetailsLayout.smoothClose(smooth); 106 | } 107 | 108 | // private class Adapter extends BaseAdapter { 109 | // 110 | // private List datas; 111 | // 112 | // Adapter(List datas) { 113 | // this.datas = datas; 114 | // } 115 | // 116 | // @Override 117 | // public int getCount() { 118 | // return null == datas ? 0 : datas.size(); 119 | // } 120 | // 121 | // @Override 122 | // public String getItem(int position) { 123 | // return null == datas ? null : datas.get(position); 124 | // } 125 | // 126 | // @Override 127 | // public long getItemId(int position) { 128 | // return position; 129 | // } 130 | // 131 | // @Override 132 | // public View getView(int position, View convertView, ViewGroup parent) { 133 | // if (null == convertView) { 134 | // convertView = getLayoutInflater().inflate(R.layout.layout_list_item, null); 135 | // } 136 | // final TextView textView = (TextView) convertView.findViewById(android.R.id.text1); 137 | // textView.setText(getItem(position)); 138 | // return convertView; 139 | // } 140 | // } 141 | } 142 | -------------------------------------------------------------------------------- /demo/src/main/java/cn/bleu/slidedetailsdemo/fragment/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.slidedetailsdemo.fragment; 2 | 3 | import android.content.Context; 4 | import android.support.v4.app.Fragment; 5 | 6 | import cn.bleu.slidedetailsdemo.ISlideCallback; 7 | 8 | /** 9 | * Project: SlideDetailsLayout
10 | * Create Date: 16/1/25
11 | * Author: Gordon
12 | * Description:
13 | */ 14 | public class BaseFragment extends Fragment { 15 | 16 | private ISlideCallback mISlideCallback; 17 | 18 | public BaseFragment() { 19 | 20 | } 21 | 22 | @Override 23 | public void onAttach(Context context) { 24 | super.onAttach(context); 25 | if (!(context instanceof ISlideCallback)) { 26 | throw new IllegalArgumentException("Your activity must be implements ISlideCallback"); 27 | } 28 | mISlideCallback = (ISlideCallback) context; 29 | } 30 | 31 | protected void open(boolean smooth) { 32 | mISlideCallback.openDetails(smooth); 33 | } 34 | 35 | protected void close(boolean smooth) { 36 | mISlideCallback.closeDetails(smooth); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /demo/src/main/java/cn/bleu/slidedetailsdemo/fragment/ListFragment.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.slidedetailsdemo.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.BaseAdapter; 9 | import android.widget.ListView; 10 | import android.widget.TextView; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import cn.bleu.slidedetailsdemo.R; 16 | 17 | /** 18 | * Project: SlideDetailsLayout
19 | * Create Date: 16/1/25
20 | * Author: Gordon
21 | * Description:
22 | */ 23 | public class ListFragment extends BaseFragment { 24 | 25 | public ListFragment() { 26 | 27 | } 28 | 29 | @Nullable 30 | @Override 31 | public View onCreateView(LayoutInflater inflater, 32 | @Nullable ViewGroup container, 33 | @Nullable Bundle savedInstanceState) { 34 | return inflater.inflate(R.layout.layout_listview, null); 35 | } 36 | 37 | @Override 38 | public void onViewCreated(View view, Bundle savedInstanceState) { 39 | super.onViewCreated(view, savedInstanceState); 40 | final ListView listView = (ListView) view.findViewById(android.R.id.list); 41 | 42 | List datas = new ArrayList<>(); 43 | for (int i = 0; i < 50; i++) { 44 | datas.add("data: " + i); 45 | } 46 | 47 | final View footView = getActivity().getLayoutInflater() 48 | .inflate(R.layout.slidedetails_marker_default_layout, null); 49 | footView.setOnClickListener(new View.OnClickListener() { 50 | @Override 51 | public void onClick(View v) { 52 | open(true); 53 | } 54 | }); 55 | 56 | listView.addFooterView(footView); 57 | listView.setAdapter(new Adapter(datas)); 58 | } 59 | 60 | private class Adapter extends BaseAdapter { 61 | 62 | private List datas; 63 | 64 | Adapter(List datas) { 65 | this.datas = datas; 66 | } 67 | 68 | @Override 69 | public int getCount() { 70 | return null == datas ? 0 : datas.size(); 71 | } 72 | 73 | @Override 74 | public String getItem(int position) { 75 | return null == datas ? null : datas.get(position); 76 | } 77 | 78 | @Override 79 | public long getItemId(int position) { 80 | return position; 81 | } 82 | 83 | @Override 84 | public View getView(int position, View convertView, ViewGroup parent) { 85 | if (null == convertView) { 86 | convertView = getActivity().getLayoutInflater().inflate(R.layout.layout_list_item, null); 87 | } 88 | final TextView textView = (TextView) convertView.findViewById(android.R.id.text1); 89 | textView.setText(getItem(position)); 90 | return convertView; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /demo/src/main/java/cn/bleu/slidedetailsdemo/fragment/ScrollViewFragment.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.slidedetailsdemo.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import cn.bleu.slidedetailsdemo.R; 10 | 11 | /** 12 | * Project: SlideDetailsLayout
13 | * Create Date: 16/1/26
14 | * Author: Gordon
15 | * Description:
16 | */ 17 | public class ScrollViewFragment extends BaseFragment { 18 | 19 | @Nullable 20 | @Override 21 | public View onCreateView(LayoutInflater inflater, 22 | @Nullable ViewGroup container, 23 | @Nullable Bundle savedInstanceState) { 24 | return inflater.inflate(R.layout.scroll_fragment, null); 25 | } 26 | 27 | @Override 28 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 29 | super.onViewCreated(view, savedInstanceState); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demo/src/main/java/cn/bleu/slidedetailsdemo/fragment/ViewPagerFragment.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.slidedetailsdemo.fragment; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.v4.view.PagerAdapter; 7 | import android.support.v4.view.ViewPager; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.TextView; 12 | 13 | import cn.bleu.slidedetailsdemo.R; 14 | 15 | /** 16 | * Project: SlideDetailsLayout
17 | * Create Date: 16/1/25
18 | * Author: Gordon
19 | * Description:
20 | */ 21 | public class ViewPagerFragment extends BaseFragment { 22 | 23 | 24 | @Nullable 25 | @Override 26 | public View onCreateView(LayoutInflater inflater, 27 | @Nullable ViewGroup container, 28 | @Nullable Bundle savedInstanceState) { 29 | return inflater.inflate(R.layout.layout_viewpager, null); 30 | } 31 | 32 | @Override 33 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 34 | super.onViewCreated(view, savedInstanceState); 35 | 36 | final ViewPager viewPager = (ViewPager) view.findViewById(R.id.viewpager); 37 | viewPager.setAdapter(new Adapter()); 38 | } 39 | 40 | 41 | private class Adapter extends PagerAdapter { 42 | 43 | @Override 44 | public int getCount() { 45 | return 5; 46 | } 47 | 48 | @Override 49 | public Object instantiateItem(ViewGroup container, int position) { 50 | final Activity activity = getActivity(); 51 | View view = activity.getLayoutInflater().inflate(R.layout.layout_list_item, null); 52 | TextView textView = (TextView) view.findViewById(android.R.id.text1); 53 | 54 | // textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 30); 55 | textView.setText(String.valueOf("data: " + position)); 56 | container.addView(view); 57 | return view; 58 | } 59 | 60 | @Override 61 | public void destroyItem(ViewGroup container, int position, Object object) { 62 | } 63 | 64 | @Override 65 | public boolean isViewFromObject(View view, Object object) { 66 | return view == object; 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/layout_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/layout_listview.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/layout_viewpager.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/scroll_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 17 | 18 | 23 | 24 | 29 | 30 | 35 | 36 | 41 | 42 | 47 | 48 | 53 | 54 | 59 | 60 | 65 | 66 | 71 | 72 | 77 | 78 | 83 | 84 | 89 | 90 | 95 | 96 | 101 | 102 | 107 | 108 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/slidedetails_marker_default_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnbleu/SlideDetailsLayout/e4d46f0b161b2a5f9418331c765627ad5f10d466/demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnbleu/SlideDetailsLayout/e4d46f0b161b2a5f9418331c765627ad5f10d466/demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnbleu/SlideDetailsLayout/e4d46f0b161b2a5f9418331c765627ad5f10d466/demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnbleu/SlideDetailsLayout/e4d46f0b161b2a5f9418331c765627ad5f10d466/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnbleu/SlideDetailsLayout/e4d46f0b161b2a5f9418331c765627ad5f10d466/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SlideDetailsDemo 3 | 4 | -------------------------------------------------------------------------------- /demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/src/test/java/cn/bleu/slidedetailsdemo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.slidedetailsdemo; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## Project-wide Gradle settings. 2 | # 3 | # For more details on how to configure your build environment visit 4 | # http://www.gradle.org/docs/current/userguide/build_environment.html 5 | # 6 | # Specifies the JVM arguments used for the daemon process. 7 | # The setting is particularly useful for tweaking memory settings. 8 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 10 | # 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | #Mon Jan 25 13:11:00 CST 2016 16 | # systemProp.http.proxyHost=127.0.0.1 17 | # systemProp.http.proxyPort=1080 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnbleu/SlideDetailsLayout/e4d46f0b161b2a5f9418331c765627ad5f10d466/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | testCompile 'junit:junit:4.12' 24 | compile 'com.android.support:appcompat-v7:23.1.1' 25 | compile 'com.android.support:support-v4:23.1.1' 26 | } 27 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/gordon/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /library/src/androidTest/java/cn/bleu/widget/slidedetails/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.widget.slidedetails; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /library/src/main/java/cn/bleu/widget/slidedetails/SlideDebug.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.widget.slidedetails; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * Project: SlideDetailsLayout
7 | * Create Date: 16/1/25
8 | * Author: Gordon
9 | * Description:
10 | */ 11 | public class SlideDebug { 12 | public static final String TAG = "slide"; 13 | public static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE); 14 | } 15 | -------------------------------------------------------------------------------- /library/src/main/java/cn/bleu/widget/slidedetails/SlideDetailsLayout.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.widget.slidedetails; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.content.res.TypedArray; 8 | import android.os.Parcel; 9 | import android.os.Parcelable; 10 | import android.support.v4.view.MotionEventCompat; 11 | import android.support.v4.view.ViewCompat; 12 | import android.util.AttributeSet; 13 | import android.util.Log; 14 | import android.view.MotionEvent; 15 | import android.view.VelocityTracker; 16 | import android.view.View; 17 | import android.view.ViewConfiguration; 18 | import android.view.ViewGroup; 19 | import android.widget.AbsListView; 20 | 21 | import static cn.bleu.widget.slidedetails.SlideDebug.DEBUG; 22 | import static cn.bleu.widget.slidedetails.SlideDebug.TAG; 23 | 24 | /** 25 | * Project: SlideDetailsLayout
26 | * Create Date: 16/1/22
27 | * Author: Gordon
28 | * Description: 29 | * Pull up to open panel, pull down to close panel. 30 | *
31 | */ 32 | @SuppressWarnings("unused") 33 | public class SlideDetailsLayout extends ViewGroup { 34 | 35 | /** 36 | * Callback for panel OPEN-CLOSE status changed. 37 | */ 38 | public interface OnSlideDetailsListener { 39 | /** 40 | * Called after status changed. 41 | * 42 | * @param status {@link Status} 43 | */ 44 | void onStatucChanged(Status status); 45 | } 46 | 47 | public enum Status { 48 | /** Panel is closed */ 49 | CLOSE, 50 | /** Panel is opened */ 51 | OPEN; 52 | 53 | public static Status valueOf(int stats) { 54 | if (0 == stats) { 55 | return CLOSE; 56 | } else if (1 == stats) { 57 | return OPEN; 58 | } else { 59 | return CLOSE; 60 | } 61 | } 62 | } 63 | 64 | private static final float DEFAULT_PERCENT = 0.2f; 65 | private static final int DEFAULT_DURATION = 300; 66 | private static final float DEFAULT_MAX_VELOCITY = 2500f; 67 | 68 | private View mFrontView; 69 | private View mBehindView; 70 | 71 | private float mTouchSlop; 72 | private float mInitMotionY; 73 | private float mInitMotionX; 74 | 75 | 76 | private View mTarget; 77 | private float mSlideOffset; 78 | private Status mStatus = Status.CLOSE; 79 | private boolean isFirstShowBehindView = true; 80 | private float mPercent = DEFAULT_PERCENT; 81 | private long mDuration = DEFAULT_DURATION; 82 | private int mDefaultPanel = 0; 83 | private VelocityTracker mVelocityTracker; 84 | 85 | private OnSlideDetailsListener mOnSlideDetailsListener; 86 | 87 | public SlideDetailsLayout(Context context) { 88 | this(context, null); 89 | } 90 | 91 | public SlideDetailsLayout(Context context, AttributeSet attrs) { 92 | this(context, attrs, 0); 93 | } 94 | 95 | public SlideDetailsLayout(Context context, AttributeSet attrs, int defStyleAttr) { 96 | super(context, attrs, defStyleAttr); 97 | 98 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlideDetailsLayout, defStyleAttr, 0); 99 | mPercent = a.getFloat(R.styleable.SlideDetailsLayout_percent, DEFAULT_PERCENT); 100 | mDuration = a.getInt(R.styleable.SlideDetailsLayout_duration, DEFAULT_DURATION); 101 | mDefaultPanel = a.getInt(R.styleable.SlideDetailsLayout_default_panel, 0); 102 | a.recycle(); 103 | 104 | mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 105 | } 106 | 107 | /** 108 | * Set the callback of panel OPEN-CLOSE status. 109 | * 110 | * @param listener {@link OnSlideDetailsListener} 111 | */ 112 | public void setOnSlideDetailsListener(OnSlideDetailsListener listener) { 113 | this.mOnSlideDetailsListener = listener; 114 | } 115 | 116 | /** 117 | * Open pannel smoothly. 118 | * 119 | * @param smooth true, smoothly. false otherwise. 120 | */ 121 | public void smoothOpen(boolean smooth) { 122 | if (mStatus != Status.OPEN) { 123 | mStatus = Status.OPEN; 124 | final float height = -getMeasuredHeight(); 125 | animatorSwitch(0, height, true, smooth ? mDuration : 0); 126 | } 127 | } 128 | 129 | /** 130 | * Close pannel smoothly. 131 | * 132 | * @param smooth true, smoothly. false otherwise. 133 | */ 134 | public void smoothClose(boolean smooth) { 135 | if (mStatus != Status.CLOSE) { 136 | mStatus = Status.OPEN; 137 | final float height = -getMeasuredHeight(); 138 | animatorSwitch(height, 0, true, smooth ? mDuration : 0); 139 | } 140 | } 141 | 142 | /** 143 | * Set the float value for indicate the moment of switch panel 144 | * 145 | * @param percent (0.0, 1.0) 146 | */ 147 | public void setPercent(float percent) { 148 | this.mPercent = percent; 149 | } 150 | 151 | @Override 152 | protected LayoutParams generateDefaultLayoutParams() { 153 | return new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT); 154 | } 155 | 156 | @Override 157 | public LayoutParams generateLayoutParams(AttributeSet attrs) { 158 | return new MarginLayoutParams(getContext(), attrs); 159 | } 160 | 161 | @Override 162 | protected LayoutParams generateLayoutParams(LayoutParams p) { 163 | return new MarginLayoutParams(p); 164 | } 165 | 166 | @Override 167 | protected void onFinishInflate() { 168 | super.onFinishInflate(); 169 | 170 | final int childCount = getChildCount(); 171 | if (1 >= childCount) { 172 | throw new RuntimeException("SlideDetailsLayout only accept childs more than 1!!"); 173 | } 174 | 175 | mFrontView = getChildAt(0); 176 | mBehindView = getChildAt(1); 177 | 178 | // set behindview's visibility to GONE before show. 179 | mBehindView.setVisibility(GONE); 180 | if (mDefaultPanel == 1) { 181 | post(new Runnable() { 182 | @Override 183 | public void run() { 184 | smoothOpen(false); 185 | } 186 | }); 187 | } 188 | } 189 | 190 | @Override 191 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 192 | final int pWidth = MeasureSpec.getSize(widthMeasureSpec); 193 | final int pHeight = MeasureSpec.getSize(heightMeasureSpec); 194 | 195 | final int childWidthMeasureSpec = 196 | MeasureSpec.makeMeasureSpec(pWidth, MeasureSpec.EXACTLY); 197 | final int childHeightMeasureSpec = 198 | MeasureSpec.makeMeasureSpec(pHeight, MeasureSpec.EXACTLY); 199 | 200 | View child; 201 | for (int i = 0; i < getChildCount(); i++) { 202 | child = getChildAt(i); 203 | // skip measure if gone 204 | if (child.getVisibility() == GONE) { 205 | continue; 206 | } 207 | 208 | measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); 209 | } 210 | 211 | setMeasuredDimension(pWidth, pHeight); 212 | } 213 | 214 | @Override 215 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 216 | final int left = l; 217 | final int right = r; 218 | int top; 219 | int bottom; 220 | 221 | final int offset = (int) mSlideOffset; 222 | 223 | View child; 224 | for (int i = 0; i < getChildCount(); i++) { 225 | child = getChildAt(i); 226 | 227 | // skip layout 228 | if (child.getVisibility() == GONE) { 229 | continue; 230 | } 231 | 232 | if (child == mBehindView) { 233 | top = b + offset; 234 | bottom = top + b - t; 235 | } else { 236 | top = t + offset; 237 | bottom = b + offset; 238 | } 239 | 240 | child.layout(left, top, right, bottom); 241 | } 242 | } 243 | 244 | @Override 245 | public boolean onInterceptTouchEvent(MotionEvent ev) { 246 | ensureTarget(); 247 | if (null == mTarget) { 248 | return false; 249 | } 250 | 251 | if (!isEnabled()) { 252 | return false; 253 | } 254 | 255 | final int aciton = MotionEventCompat.getActionMasked(ev); 256 | 257 | boolean shouldIntercept = false; 258 | switch (aciton) { 259 | case MotionEvent.ACTION_DOWN: { 260 | if (null == mVelocityTracker) { 261 | mVelocityTracker = VelocityTracker.obtain(); 262 | } else { 263 | mVelocityTracker.clear(); 264 | } 265 | mVelocityTracker.addMovement(ev); 266 | 267 | mInitMotionX = ev.getX(); 268 | mInitMotionY = ev.getY(); 269 | shouldIntercept = false; 270 | break; 271 | } 272 | case MotionEvent.ACTION_MOVE: { 273 | final float x = ev.getX(); 274 | final float y = ev.getY(); 275 | 276 | final float xDiff = x - mInitMotionX; 277 | final float yDiff = y - mInitMotionY; 278 | 279 | if (canChildScrollVertically((int) yDiff)) { 280 | shouldIntercept = false; 281 | if (DEBUG) { 282 | Log.d(TAG, "intercept, child can scroll vertically, do not intercept"); 283 | } 284 | } else { 285 | final float xDiffabs = Math.abs(xDiff); 286 | final float yDiffabs = Math.abs(yDiff); 287 | 288 | // intercept rules: 289 | // 1. The vertical displacement is larger than the horizontal displacement; 290 | // 2. Panel stauts is CLOSE:slide up 291 | // 3. Panel status is OPEN:slide down 292 | if (yDiffabs > mTouchSlop && yDiffabs >= xDiffabs 293 | && !(mStatus == Status.CLOSE && yDiff > 0 294 | || mStatus == Status.OPEN && yDiff < 0)) { 295 | shouldIntercept = true; 296 | if (DEBUG) { 297 | Log.d(TAG, "intercept, intercept events"); 298 | } 299 | } 300 | } 301 | break; 302 | } 303 | case MotionEvent.ACTION_UP: 304 | case MotionEvent.ACTION_CANCEL: { 305 | shouldIntercept = false; 306 | break; 307 | } 308 | 309 | } 310 | 311 | return shouldIntercept; 312 | } 313 | 314 | private void recycleVelocityTracker(){ 315 | if(null != mVelocityTracker){ 316 | mVelocityTracker.recycle(); 317 | mVelocityTracker = null; 318 | } 319 | } 320 | 321 | @Override 322 | public boolean onTouchEvent(MotionEvent ev) { 323 | ensureTarget(); 324 | if (null == mTarget) { 325 | return false; 326 | } 327 | 328 | if (!isEnabled()) { 329 | return false; 330 | } 331 | 332 | 333 | boolean wantTouch = true; 334 | final int action = MotionEventCompat.getActionMasked(ev); 335 | 336 | switch (action) { 337 | case MotionEvent.ACTION_DOWN: { 338 | // if target is a view, we want the DOWN action. 339 | if (mTarget instanceof View) { 340 | wantTouch = true; 341 | } 342 | break; 343 | } 344 | 345 | case MotionEvent.ACTION_MOVE: { 346 | mVelocityTracker.addMovement(ev); 347 | mVelocityTracker.computeCurrentVelocity(1000); 348 | final float y = ev.getY(); 349 | final float yDiff = y - mInitMotionY; 350 | if (canChildScrollVertically(((int) yDiff))) { 351 | wantTouch = false; 352 | } else { 353 | processTouchEvent(yDiff); 354 | wantTouch = true; 355 | } 356 | break; 357 | } 358 | 359 | case MotionEvent.ACTION_UP: 360 | case MotionEvent.ACTION_CANCEL: { 361 | finishTouchEvent(); 362 | recycleVelocityTracker(); 363 | wantTouch = false; 364 | break; 365 | } 366 | } 367 | return wantTouch; 368 | } 369 | 370 | /** 371 | * @param offset Displacement in vertically. 372 | */ 373 | private void processTouchEvent(final float offset) { 374 | if (Math.abs(offset) < mTouchSlop) { 375 | return; 376 | } 377 | 378 | final float oldOffset = mSlideOffset; 379 | // pull up to open 380 | if (mStatus == Status.CLOSE) { 381 | // reset if pull down 382 | if (offset >= 0) { 383 | mSlideOffset = 0; 384 | } else { 385 | mSlideOffset = offset; 386 | } 387 | 388 | if (mSlideOffset == oldOffset) { 389 | return; 390 | } 391 | 392 | // pull down to close 393 | } else if (mStatus == Status.OPEN) { 394 | final float pHeight = -getMeasuredHeight(); 395 | // reset if pull up 396 | if (offset <= 0) { 397 | mSlideOffset = pHeight; 398 | } else { 399 | final float newOffset = pHeight + offset; 400 | mSlideOffset = newOffset; 401 | } 402 | 403 | if (mSlideOffset == oldOffset) { 404 | return; 405 | } 406 | } 407 | 408 | if (SlideDebug.DEBUG) { 409 | Log.v("slide", "process, offset: " + mSlideOffset); 410 | } 411 | // relayout 412 | requestLayout(); 413 | } 414 | 415 | /** 416 | * Called after gesture is ending. 417 | */ 418 | private void finishTouchEvent() { 419 | final int pHeight = getMeasuredHeight(); 420 | final int percent = (int) (pHeight * mPercent); 421 | final float offset = mSlideOffset; 422 | 423 | boolean changed = false; 424 | final float yVelocity = mVelocityTracker.getYVelocity(); 425 | 426 | if (DEBUG) { 427 | Log.v(TAG, "finish, offset: " + offset + ", percent: " + percent + ", yVelocity: " + yVelocity); 428 | } 429 | 430 | if (Status.CLOSE == mStatus) { 431 | if (offset <= -percent || yVelocity <= -DEFAULT_MAX_VELOCITY) { 432 | mSlideOffset = -pHeight; 433 | mStatus = Status.OPEN; 434 | changed = true; 435 | } else { 436 | // keep panel closed 437 | mSlideOffset = 0; 438 | } 439 | } else if (Status.OPEN == mStatus) { 440 | if ((offset + pHeight) >= percent || yVelocity >= DEFAULT_MAX_VELOCITY) { 441 | mSlideOffset = 0; 442 | mStatus = Status.CLOSE; 443 | changed = true; 444 | } else { 445 | // keep panel opened 446 | mSlideOffset = -pHeight; 447 | } 448 | } 449 | 450 | animatorSwitch(offset, mSlideOffset, changed); 451 | } 452 | 453 | private void animatorSwitch(final float start, final float end) { 454 | animatorSwitch(start, end, true, mDuration); 455 | } 456 | 457 | private void animatorSwitch(final float start, final float end, final long duration) { 458 | animatorSwitch(start, end, true, duration); 459 | } 460 | 461 | private void animatorSwitch(final float start, final float end, final boolean changed) { 462 | animatorSwitch(start, end, changed, mDuration); 463 | } 464 | 465 | private void animatorSwitch(final float start, 466 | final float end, 467 | final boolean changed, 468 | final long duration) { 469 | ValueAnimator animator = ValueAnimator.ofFloat(start, end); 470 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 471 | @Override 472 | public void onAnimationUpdate(ValueAnimator animation) { 473 | mSlideOffset = (float) animation.getAnimatedValue(); 474 | requestLayout(); 475 | } 476 | }); 477 | animator.addListener(new AnimatorListenerAdapter() { 478 | @Override 479 | public void onAnimationEnd(Animator animation) { 480 | super.onAnimationEnd(animation); 481 | if (changed) { 482 | if (mStatus == Status.OPEN) { 483 | checkAndFirstOpenPanel(); 484 | } 485 | 486 | if (null != mOnSlideDetailsListener) { 487 | mOnSlideDetailsListener.onStatucChanged(mStatus); 488 | } 489 | } 490 | } 491 | }); 492 | animator.setDuration(duration); 493 | animator.start(); 494 | } 495 | 496 | /** 497 | * Whether the closed pannel is opened at first time. 498 | * If open first, we should set the behind view's visibility as VISIBLE. 499 | */ 500 | private void checkAndFirstOpenPanel() { 501 | if (isFirstShowBehindView) { 502 | isFirstShowBehindView = false; 503 | mBehindView.setVisibility(VISIBLE); 504 | } 505 | } 506 | 507 | /** 508 | * When pulling, target view changed by the panel status. If panel opened, the target is behind view. 509 | * Front view is for otherwise. 510 | */ 511 | private void ensureTarget() { 512 | if (mStatus == Status.CLOSE) { 513 | mTarget = mFrontView; 514 | } else { 515 | mTarget = mBehindView; 516 | } 517 | } 518 | 519 | /** 520 | * Check child view can srcollable in vertical direction. 521 | * 522 | * @param direction Negative to check scrolling up, positive to check scrolling down. 523 | * 524 | * @return true if this view can be scrolled in the specified direction, false otherwise. 525 | */ 526 | protected boolean canChildScrollVertically(int direction) { 527 | return innerCanChildScrollVertically(mTarget, -direction); 528 | } 529 | 530 | private boolean innerCanChildScrollVertically(View view, int direction) { 531 | if (view instanceof ViewGroup) { 532 | final ViewGroup vGroup = (ViewGroup) view; 533 | View child; 534 | boolean result; 535 | for (int i = 0; i < vGroup.getChildCount(); i++) { 536 | child = vGroup.getChildAt(i); 537 | if (child instanceof View) { 538 | result = ViewCompat.canScrollVertically(child, direction); 539 | } else { 540 | result = innerCanChildScrollVertically(child, direction); 541 | } 542 | 543 | if (result) { 544 | return true; 545 | } 546 | } 547 | } 548 | 549 | return ViewCompat.canScrollVertically(view, direction); 550 | } 551 | 552 | protected boolean canListViewSroll(AbsListView absListView) { 553 | if (mStatus == Status.OPEN) { 554 | return absListView.getChildCount() > 0 555 | && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 556 | .getTop() < 557 | absListView.getPaddingTop()); 558 | } else { 559 | final int count = absListView.getChildCount(); 560 | return count > 0 561 | && (absListView.getLastVisiblePosition() < count - 1 562 | || absListView.getChildAt(count - 1) 563 | .getBottom() > absListView.getMeasuredHeight()); 564 | } 565 | } 566 | 567 | @Override 568 | protected Parcelable onSaveInstanceState() { 569 | SavedState ss = new SavedState(super.onSaveInstanceState()); 570 | ss.offset = mSlideOffset; 571 | ss.status = mStatus.ordinal(); 572 | return ss; 573 | } 574 | 575 | @Override 576 | protected void onRestoreInstanceState(Parcelable state) { 577 | SavedState ss = (SavedState) state; 578 | super.onRestoreInstanceState(ss.getSuperState()); 579 | mSlideOffset = ss.offset; 580 | mStatus = Status.valueOf(ss.status); 581 | 582 | if (mStatus == Status.OPEN) { 583 | mBehindView.setVisibility(VISIBLE); 584 | } 585 | 586 | requestLayout(); 587 | } 588 | 589 | static class SavedState extends BaseSavedState { 590 | 591 | private float offset; 592 | private int status; 593 | 594 | /** 595 | * Constructor used when reading from a parcel. Reads the state of the superclass. 596 | * 597 | * @param source 598 | */ 599 | public SavedState(Parcel source) { 600 | super(source); 601 | offset = source.readFloat(); 602 | status = source.readInt(); 603 | } 604 | 605 | /** 606 | * Constructor called by derived classes when creating their SavedState objects 607 | * 608 | * @param superState The state of the superclass of this view 609 | */ 610 | public SavedState(Parcelable superState) { 611 | super(superState); 612 | } 613 | 614 | @Override 615 | public void writeToParcel(Parcel out, int flags) { 616 | super.writeToParcel(out, flags); 617 | out.writeFloat(offset); 618 | out.writeInt(status); 619 | } 620 | 621 | public static final Parcelable.Creator CREATOR = 622 | new Parcelable.Creator() { 623 | public SavedState createFromParcel(Parcel in) { 624 | return new SavedState(in); 625 | } 626 | 627 | public SavedState[] newArray(int size) { 628 | return new SavedState[size]; 629 | } 630 | }; 631 | } 632 | } 633 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /library/src/test/java/cn/bleu/widget/slidedetails/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package cn.bleu.widget.slidedetails; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':demo', ':library' 2 | --------------------------------------------------------------------------------