├── demo ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── drawable │ │ │ ├── tu.jpg │ │ │ ├── text_view_bg.xml │ │ │ └── ic_launcher_background.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── xml │ │ │ └── network_security_config.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── layout │ │ │ ├── block_state_text_view.xml │ │ │ ├── block_focus_state_view.xml │ │ │ ├── title_view.xml │ │ │ ├── activity_main.xml │ │ │ ├── block_image.xml │ │ │ ├── footer_view.xml │ │ │ └── block_loading_state.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ └── com │ │ │ └── msisuzney │ │ │ └── tv │ │ │ └── demo │ │ │ ├── bean │ │ │ ├── ColumnFocusStateBean.java │ │ │ ├── RecyclerViewStateBean.java │ │ │ ├── FooterBean.java │ │ │ ├── TitleBean.java │ │ │ └── TabBean.java │ │ │ ├── MyStateChangedObserver.java │ │ │ ├── MyStateChangeObservable.java │ │ │ ├── MainActivity.java │ │ │ ├── viewfactory │ │ │ ├── presenter │ │ │ │ ├── FooterViewPresenter.java │ │ │ │ ├── ColumnFocusChangeListenerTextViewPresenter.java │ │ │ │ ├── TitlePresenter.java │ │ │ │ ├── StateTextViewPresenter.java │ │ │ │ ├── ImageViewPresenter.java │ │ │ │ └── ImageViewPresenter2.java │ │ │ └── ColumnItemViewFactory.java │ │ │ ├── view │ │ │ ├── StateTextView.java │ │ │ └── ColumnFocusChangeListenerTextView.java │ │ │ └── WaterfallFragment.java │ │ ├── AndroidManifest.xml │ │ └── assets │ │ └── data.json ├── proguard-rules.pro └── build.gradle ├── waterfallayout ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── values │ │ │ └── strings.xml │ │ └── layout │ │ │ ├── hgv.xml │ │ │ └── fragment_waterfall.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── msisuzney │ │ └── tv │ │ └── waterfallayout │ │ ├── OnItemKeyListener.java │ │ ├── OnRowSelectedListener.java │ │ ├── model │ │ ├── Item.java │ │ ├── ColumnLayoutCollection.java │ │ ├── HorizontalLayoutItem.java │ │ ├── HorizontalLayoutCollection.java │ │ ├── Collection.java │ │ └── ColumnLayoutItem.java │ │ ├── leanback │ │ ├── ViewHolderTask.java │ │ ├── PresenterSelector.java │ │ ├── FacetProvider.java │ │ ├── FacetProviderAdapter.java │ │ ├── OnChildLaidOutListener.java │ │ ├── SinglePresenterSelector.java │ │ ├── OnChildSelectedListener.java │ │ ├── ItemAlignment.java │ │ ├── OnChildViewHolderSelectedListener.java │ │ ├── VerticalGridView.java │ │ ├── package-info.java │ │ ├── ItemAlignmentFacetHelper.java │ │ ├── ArrayObjectAdapter.java │ │ ├── ItemAlignmentFacet.java │ │ ├── SingleRow.java │ │ ├── ViewsStateBundle.java │ │ ├── Presenter.java │ │ ├── ObjectAdapter.java │ │ └── WindowAlignment.java │ │ ├── StateChangedObserver.java │ │ ├── presenter │ │ ├── RowPresenterSelector.java │ │ ├── ColumnLayoutRowPresenter.java │ │ └── HorizontalLayoutRowPresenter.java │ │ ├── view │ │ ├── ColumnFocusChangeListener.java │ │ ├── ColumnLayout.java │ │ └── FocusLineFeedFrameLayout.java │ │ ├── StateChangeObservable.java │ │ └── RowsFragment.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── demo.gif ├── mpv.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | **/.DS_Store -------------------------------------------------------------------------------- /waterfallayout/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | **/.DS_Store 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':demo', ':waterfallayout' 2 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo.gif -------------------------------------------------------------------------------- /mpv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/mpv.png -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TV_waterfallayout 3 | 4 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/tu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/drawable/tu.jpg -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /waterfallayout/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Waterfall 3 | 4 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msisuzney/tv-waterfall-layout/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /waterfallayout/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /demo/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/bean/ColumnFocusStateBean.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.bean; 2 | 3 | /** 4 | * 栏目中监听栏目是否获得焦点的Bean 5 | * @author: chenxin 6 | * @date: 2020-02-29 7 | * @email: chenxin7930@qq.com 8 | */ 9 | public class ColumnFocusStateBean { 10 | } 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 26 20:24:23 CST 2019 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-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/bean/RecyclerViewStateBean.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.bean; 2 | 3 | /** 4 | * 栏目中监听RecyclerView状态的Bean 5 | * @author: chenxin 6 | * @date: 2019-12-21 7 | * @email: chenxin7930@qq.com 8 | */ 9 | public class RecyclerViewStateBean { 10 | } 11 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/text_view_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/MyStateChangedObserver.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo; 2 | 3 | 4 | import com.msisuzney.tv.waterfallayout.StateChangedObserver; 5 | 6 | /** 7 | * @author: chenxin 8 | * @date: 2019-12-20 9 | * @email: chenxin7930@qq.com 10 | */ 11 | public interface MyStateChangedObserver extends StateChangedObserver { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/MyStateChangeObservable.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo; 2 | 3 | 4 | import com.msisuzney.tv.waterfallayout.StateChangeObservable; 5 | 6 | /** 7 | * @author: chenxin 8 | * @date: 2019-12-20 9 | * @email: chenxin7930@qq.com 10 | */ 11 | public class MyStateChangeObservable extends StateChangeObservable { 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/bean/FooterBean.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.bean; 2 | /** 3 | * @author: chenxin 4 | * @date: 2019-12-20 5 | * @email: chenxin7930@qq.com 6 | */ 7 | public class FooterBean { 8 | 9 | private String text; 10 | 11 | public String getText() { 12 | return text; 13 | } 14 | 15 | public void setText(String text) { 16 | this.text = text; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/OnItemKeyListener.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout; 2 | 3 | import android.view.KeyEvent; 4 | import android.view.View; 5 | /** 6 | * @author: chenxin 7 | * @date: 2019-12-20 8 | * @email: chenxin7930@qq.com 9 | */ 10 | public interface OnItemKeyListener { 11 | void onClick(Object item); 12 | 13 | boolean onKey(View v, KeyEvent event, Object item); 14 | } 15 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/OnRowSelectedListener.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout; 2 | 3 | /** 4 | * @author: chenxin 5 | * @date: 2019-12-20 6 | * @email: chenxin7930@qq.com 7 | */ 8 | 9 | import com.msisuzney.tv.waterfallayout.leanback.OnChildViewHolderSelectedListener; 10 | 11 | /** 12 | * 选中某行 13 | */ 14 | public class OnRowSelectedListener extends OnChildViewHolderSelectedListener { 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/model/Item.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.model; 2 | 3 | /** 4 | * @author: chenxin 5 | * @date: 2019-12-20 6 | * @email: chenxin7930@qq.com 7 | */ 8 | public abstract class Item { 9 | 10 | //具体的数据 11 | private Object data; 12 | 13 | public Object getData() { 14 | return data; 15 | } 16 | 17 | public void setData(Object data) { 18 | this.data = data; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/block_state_text_view.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /waterfallayout/src/main/res/layout/hgv.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/bean/TitleBean.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.bean; 2 | /** 3 | * @author: chenxin 4 | * @date: 2019-12-20 5 | * @email: chenxin7930@qq.com 6 | */ 7 | public class TitleBean { 8 | private String title; 9 | 10 | public TitleBean(String title) { 11 | this.title = title; 12 | } 13 | 14 | public String getTitle() { 15 | return title; 16 | } 17 | 18 | public void setTitle(String title) { 19 | this.title = title; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | /** 6 | * @author: chenxin 7 | * @date: 2019-12-20 8 | * @email: chenxin7930@qq.com 9 | */ 10 | public class MainActivity extends AppCompatActivity { 11 | 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/block_focus_state_view.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /waterfallayout/src/main/res/layout/fragment_waterfall.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/title_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/model/ColumnLayoutCollection.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.model; 2 | 3 | import java.util.List; 4 | 5 | public final class ColumnLayoutCollection extends Collection { 6 | 7 | 8 | public ColumnLayoutCollection(int width, int height) { 9 | super(width, height); 10 | } 11 | 12 | private List items; 13 | 14 | public List getItems() { 15 | return items; 16 | } 17 | 18 | public void setItems(List items) { 19 | this.items = items; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/model/HorizontalLayoutItem.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.model; 2 | /** 3 | * @author: chenxin 4 | * @date: 2019-12-20 5 | * @email: chenxin7930@qq.com 6 | */ 7 | public final class HorizontalLayoutItem extends Item { 8 | //宽高 9 | private int width, height; 10 | 11 | public int getWidth() { 12 | return width; 13 | } 14 | 15 | public void setWidth(int width) { 16 | this.width = width; 17 | } 18 | 19 | public int getHeight() { 20 | return height; 21 | } 22 | 23 | public void setHeight(int height) { 24 | this.height = height; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/model/HorizontalLayoutCollection.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.model; 2 | 3 | import java.util.List; 4 | /** 5 | * @author: chenxin 6 | * @date: 2019-12-20 7 | * @email: chenxin7930@qq.com 8 | */ 9 | public final class HorizontalLayoutCollection extends Collection { 10 | 11 | public HorizontalLayoutCollection(int width, int height) { 12 | super(width, height); 13 | } 14 | 15 | private List items; 16 | 17 | public List getItems() { 18 | return items; 19 | } 20 | 21 | public void setItems(List items) { 22 | this.items = items; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/model/Collection.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.model; 2 | /** 3 | * @author: chenxin 4 | * @date: 2019-12-20 5 | * @email: chenxin7930@qq.com 6 | */ 7 | public abstract class Collection { 8 | //行的宽高 9 | private int width, height; 10 | 11 | public Collection(int width, int height) { 12 | this.width = width; 13 | this.height = height; 14 | } 15 | 16 | public int getWidth() { 17 | return width; 18 | } 19 | 20 | public void setWidth(int width) { 21 | this.width = width; 22 | } 23 | 24 | public int getHeight() { 25 | return height; 26 | } 27 | 28 | public void setHeight(int height) { 29 | this.height = height; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /demo/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | 17 | 18 | -------------------------------------------------------------------------------- /waterfallayout/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /waterfallayout/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | compileOptions { 13 | sourceCompatibility JavaVersion.VERSION_1_8 14 | targetCompatibility JavaVersion.VERSION_1_8 15 | } 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | lintOptions { 25 | abortOnError false 26 | } 27 | 28 | } 29 | 30 | dependencies { 31 | implementation 'androidx.recyclerview:recyclerview:1.0.0' 32 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 33 | } 34 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/viewfactory/presenter/FooterViewPresenter.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.viewfactory.presenter; 2 | 3 | import com.msisuzney.tv.demo.R; 4 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 5 | import android.view.LayoutInflater; 6 | import android.view.ViewGroup; 7 | 8 | /** 9 | * @author: chenxin 10 | * @date: 2019-12-20 11 | * @email: chenxin7930@qq.com 12 | */ 13 | public class FooterViewPresenter extends Presenter { 14 | @Override 15 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 16 | return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.footer_view, parent, false)); 17 | } 18 | 19 | @Override 20 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 21 | 22 | } 23 | 24 | @Override 25 | public void onUnbindViewHolder(ViewHolder viewHolder) { 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/viewfactory/presenter/ColumnFocusChangeListenerTextViewPresenter.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.viewfactory.presenter; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.ViewGroup; 5 | 6 | import com.msisuzney.tv.demo.R; 7 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 8 | 9 | /** 10 | * @author: chenxin 11 | * @date: 2020-02-29 12 | * @email: chenxin7930@qq.com 13 | */ 14 | public class ColumnFocusChangeListenerTextViewPresenter extends Presenter { 15 | @Override 16 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 17 | return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.block_focus_state_view, parent, false)); 18 | } 19 | 20 | @Override 21 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 22 | 23 | } 24 | 25 | @Override 26 | public void onUnbindViewHolder(ViewHolder viewHolder) { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/model/ColumnLayoutItem.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.model; 2 | /** 3 | * @author: chenxin 4 | * @date: 2019-12-20 5 | * @email: chenxin7930@qq.com 6 | */ 7 | public final class ColumnLayoutItem extends Item { 8 | 9 | //位置和宽高 10 | private int x, y, width, height; 11 | 12 | public int getX() { 13 | return x; 14 | } 15 | 16 | public void setX(int x) { 17 | this.x = x; 18 | } 19 | 20 | public int getY() { 21 | return y; 22 | } 23 | 24 | public void setY(int y) { 25 | this.y = y; 26 | } 27 | 28 | public int getWidth() { 29 | return width; 30 | } 31 | 32 | public void setWidth(int width) { 33 | this.width = width; 34 | } 35 | 36 | public int getHeight() { 37 | return height; 38 | } 39 | 40 | public void setHeight(int height) { 41 | this.height = height; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/ViewHolderTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import androidx.recyclerview.widget.RecyclerView; 17 | 18 | /** 19 | * Interface for schedule task on a ViewHolder. 20 | */ 21 | public interface ViewHolderTask { 22 | public void run(RecyclerView.ViewHolder viewHolder); 23 | } -------------------------------------------------------------------------------- /demo/src/main/res/layout/block_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 19 | 20 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 chenxin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/footer_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 23 | 24 | 31 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/block_loading_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/StateChangedObserver.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout; 2 | 3 | 4 | import androidx.recyclerview.widget.RecyclerView; 5 | /** 6 | * @author: chenxin 7 | * @date: 2019-12-20 8 | * @email: chenxin7930@qq.com 9 | */ 10 | /** 11 | * 状态变化监听类 12 | */ 13 | public interface StateChangedObserver { 14 | 15 | /** 16 | * fragment is being paused 17 | */ 18 | default void onFragmentPause() { 19 | } 20 | 21 | /** 22 | * Visibility of the fragment is changed 23 | * 24 | * @param isVisible 25 | */ 26 | default void onFragmentVisibilityChanged(boolean isVisible) { 27 | } 28 | 29 | /** 30 | * Callback method to be invoked when RecyclerView's scroll state changes. 31 | * 32 | * @param recyclerView The RecyclerView whose scroll state has changed. 33 | * // * @param newState The updated scroll state. One of {SCROLL_STATE_IDLE}, 34 | * // * { SCROLL_STATE_DRAGGING} or {SCROLL_STATE_SETTLING}. 35 | */ 36 | default void onScrollStateChanged(RecyclerView recyclerView, int newState) { 37 | } 38 | 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/PresenterSelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | /** 17 | * A PresenterSelector is used to obtain a {@link Presenter} for a given Object. 18 | * Similar to {@link Presenter}, PresenterSelector is stateless. 19 | */ 20 | public abstract class PresenterSelector { 21 | /** 22 | * Returns a presenter for the given item. 23 | */ 24 | public abstract Presenter getPresenter(Object item); 25 | 26 | /** 27 | * Returns an array of all possible presenters. The returned array should 28 | * not be modified. 29 | */ 30 | public Presenter[] getPresenters() { 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/FacetProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | /** 17 | * This is the query interface to supply optional features(aka facets) on an object without the need 18 | * of letting the object to subclass or implement java interfaces. 19 | */ 20 | public interface FacetProvider { 21 | 22 | /** 23 | * Queries optional implemented facet. 24 | * @param facetClass Facet classes to query, examples are: class of 25 | * {@link ItemAlignmentFacet}. 26 | * @return Facet implementation for the facetClass or null if feature not implemented. 27 | */ 28 | public Object getFacet(Class facetClass); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.msisuzney.tv.demo" 7 | minSdkVersion 15 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | compileOptions { 13 | sourceCompatibility JavaVersion.VERSION_1_8 14 | targetCompatibility JavaVersion.VERSION_1_8 15 | } 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation 'androidx.appcompat:appcompat:1.0.0' 28 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 29 | implementation 'com.github.bumptech.glide:glide:4.10.0' 30 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 31 | implementation("com.squareup.okhttp3:okhttp:4.2.2") 32 | annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0' 33 | implementation 'androidx.recyclerview:recyclerview:1.0.0' 34 | implementation 'com.google.code.gson:gson:2.8.5' 35 | implementation 'com.github.msisuzney:tv-waterfall-layout:1.0.1' 36 | // implementation project(':waterfallayout') 37 | } 38 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/view/StateTextView.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.TextView; 6 | 7 | import androidx.appcompat.widget.AppCompatTextView; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | import com.msisuzney.tv.demo.MyStateChangedObserver; 11 | 12 | /** 13 | * @author: chenxin 14 | * @date: 2019-12-21 15 | * @email: chenxin7930@qq.com 16 | */ 17 | public class StateTextView extends AppCompatTextView implements MyStateChangedObserver { 18 | public StateTextView(Context context) { 19 | super(context); 20 | } 21 | 22 | public StateTextView(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | } 25 | 26 | public StateTextView(Context context, AttributeSet attrs, int defStyleAttr) { 27 | super(context, attrs, defStyleAttr); 28 | } 29 | 30 | @Override 31 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 32 | if (newState == RecyclerView.SCROLL_STATE_IDLE) { 33 | setText("RecyclerView STATE == SCROLL_STATE_IDLE"); 34 | } else if (newState == RecyclerView.SCROLL_STATE_SETTLING) { 35 | setText("RecyclerView STATE == SCROLL_STATE_SETTLING"); 36 | } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { 37 | setText("RecyclerView STATE == SCROLL_STATE_DRAGGING"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/FacetProviderAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import androidx.recyclerview.widget.RecyclerView; 17 | 18 | /** 19 | * Optional interface that implemented by {@link RecyclerView.Adapter} to 20 | * query {@link FacetProvider} for a given type within Adapter. Note that 21 | * {@link RecyclerView.ViewHolder} may also implement {@link FacetProvider} which 22 | * has a higher priority than the one returned from the FacetProviderAdapter. 23 | */ 24 | public interface FacetProviderAdapter { 25 | 26 | /** 27 | * Queries {@link FacetProvider} for a given type within Adapter. 28 | * @param type type of the item. 29 | * @return Facet provider for the type. 30 | */ 31 | public FacetProvider getFacetProvider(int type); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/viewfactory/presenter/TitlePresenter.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.viewfactory.presenter; 2 | 3 | import com.msisuzney.tv.demo.R; 4 | import com.msisuzney.tv.demo.bean.TitleBean; 5 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 6 | 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.TextView; 11 | 12 | 13 | /** 14 | * @author: chenxin 15 | * @date: 2019-12-20 16 | * @email: chenxin7930@qq.com 17 | */ 18 | public class TitlePresenter extends Presenter { 19 | @Override 20 | public MyViewHolder onCreateViewHolder(ViewGroup parent) { 21 | return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.title_view, parent, false)); 22 | } 23 | 24 | @Override 25 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 26 | if (viewHolder instanceof MyViewHolder) { 27 | MyViewHolder vh = (MyViewHolder) viewHolder; 28 | TitleBean titleBean = (TitleBean) item; 29 | vh.titleTV.setText(titleBean.getTitle()); 30 | } 31 | } 32 | 33 | @Override 34 | public void onUnbindViewHolder(ViewHolder viewHolder) { 35 | 36 | } 37 | 38 | public static class MyViewHolder extends ViewHolder { 39 | TextView titleTV; 40 | 41 | public MyViewHolder(View view) { 42 | super(view); 43 | titleTV = (TextView) view; 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/OnChildLaidOutListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | 19 | /** 20 | * Interface for receiving notification when a child of this 21 | * ViewGroup has been laid out. 22 | */ 23 | public interface OnChildLaidOutListener { 24 | /** 25 | * Callback method to be invoked when a child of this ViewGroup has been 26 | * added to the view hierarchy and has been laid out. 27 | * 28 | * @param parent The ViewGroup where the layout happened. 29 | * @param view The view within the ViewGroup that was laid out. 30 | * @param position The position of the view in the adapter. 31 | * @param id The id of the child. 32 | */ 33 | void onChildLaidOut(ViewGroup parent, View view, int position, long id); 34 | } 35 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/SinglePresenterSelector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | /** 17 | * A {@link PresenterSelector} that always returns the same {@link Presenter}. 18 | * Useful for rows of items of the same type that are all rendered the same way. 19 | */ 20 | public final class SinglePresenterSelector extends PresenterSelector { 21 | 22 | private final Presenter mPresenter; 23 | 24 | /** 25 | * @param presenter The Presenter to return for every item. 26 | */ 27 | public SinglePresenterSelector(Presenter presenter) { 28 | mPresenter = presenter; 29 | } 30 | 31 | @Override 32 | public Presenter getPresenter(Object item) { 33 | return mPresenter; 34 | } 35 | 36 | @Override 37 | public Presenter[] getPresenters() { 38 | return new Presenter[]{mPresenter}; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/OnChildSelectedListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | 19 | /** 20 | * Interface for receiving notification when a child of this 21 | * ViewGroup has been selected. 22 | * @deprecated Use {@link OnChildViewHolderSelectedListener} 23 | */ 24 | @Deprecated 25 | public interface OnChildSelectedListener { 26 | /** 27 | * Callback method to be invoked when a child of this ViewGroup has been 28 | * selected. 29 | * 30 | * @param parent The ViewGroup where the selection happened. 31 | * @param view The view within the ViewGroup that is selected, or null if no 32 | * view is selected. 33 | * @param position The position of the view in the adapter, or NO_POSITION 34 | * if no view is selected. 35 | * @param id The id of the child that is selected, or NO_ID if no view is 36 | * selected. 37 | */ 38 | void onChildSelected(ViewGroup parent, View view, int position, long id); 39 | } 40 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/viewfactory/presenter/StateTextViewPresenter.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.viewfactory.presenter; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.ViewGroup; 5 | 6 | import com.msisuzney.tv.demo.MyStateChangeObservable; 7 | import com.msisuzney.tv.demo.R; 8 | import com.msisuzney.tv.demo.bean.RecyclerViewStateBean; 9 | import com.msisuzney.tv.demo.view.StateTextView; 10 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 11 | 12 | /** 13 | * @author: chenxin 14 | * @date: 2019-12-21 15 | * @email: chenxin7930@qq.com 16 | */ 17 | public class StateTextViewPresenter extends Presenter { 18 | 19 | private MyStateChangeObservable stateChangeObservable; 20 | 21 | public StateTextViewPresenter(MyStateChangeObservable stateChangeObservable) { 22 | this.stateChangeObservable = stateChangeObservable; 23 | } 24 | 25 | @Override 26 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 27 | return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.block_state_text_view, parent, false)); 28 | } 29 | 30 | @Override 31 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 32 | if (item instanceof RecyclerViewStateBean) { 33 | StateTextView stateTextView = (StateTextView) viewHolder.view; 34 | if (stateChangeObservable != null) { 35 | stateChangeObservable.registerObserver(stateTextView); 36 | } 37 | } 38 | } 39 | 40 | @Override 41 | public void onUnbindViewHolder(ViewHolder viewHolder) { 42 | StateTextView stateTextView = (StateTextView) viewHolder.view; 43 | if (stateChangeObservable != null) { 44 | stateChangeObservable.unregisterObserver(stateTextView); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/presenter/RowPresenterSelector.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.presenter; 2 | 3 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 4 | import com.msisuzney.tv.waterfallayout.leanback.PresenterSelector; 5 | 6 | import com.msisuzney.tv.waterfallayout.model.ColumnLayoutCollection; 7 | import com.msisuzney.tv.waterfallayout.model.HorizontalLayoutCollection; 8 | /** 9 | * @author: chenxin 10 | * @date: 2019-12-20 11 | * @email: chenxin7930@qq.com 12 | */ 13 | public final class RowPresenterSelector extends PresenterSelector { 14 | 15 | private ColumnLayoutRowPresenter absLayoutColumnLayoutRowPresenter; 16 | private HorizontalLayoutRowPresenter horizontalLayoutRowPresenter; 17 | private PresenterSelector otherPresenterSelector; 18 | 19 | public RowPresenterSelector(PresenterSelector blockPresenterSelector) { 20 | absLayoutColumnLayoutRowPresenter = new ColumnLayoutRowPresenter(blockPresenterSelector); 21 | horizontalLayoutRowPresenter = new HorizontalLayoutRowPresenter(blockPresenterSelector); 22 | } 23 | 24 | 25 | /** 26 | * 瀑布流中不是行的选择器,比如加载更多的提示View 27 | * 28 | * @param otherPresenterSelector 29 | */ 30 | public void setOtherPresenterSelector(PresenterSelector otherPresenterSelector) { 31 | this.otherPresenterSelector = otherPresenterSelector; 32 | } 33 | 34 | 35 | @Override 36 | public Presenter getPresenter(Object item) { 37 | if (item instanceof ColumnLayoutCollection) { 38 | return absLayoutColumnLayoutRowPresenter; 39 | } else if (item instanceof HorizontalLayoutCollection) { 40 | return horizontalLayoutRowPresenter; 41 | } else { 42 | return otherPresenterSelector.getPresenter(item); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/view/ColumnFocusChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.view; 2 | 3 | import android.view.View; 4 | import android.view.ViewParent; 5 | 6 | /** 7 | * 可以监听整个栏目焦点变化的接口 8 | * 9 | * @author: chenxin 10 | * @date: 2020-01-06 11 | */ 12 | public interface ColumnFocusChangeListener { 13 | 14 | void onColumnGainFocus(); 15 | 16 | void onColumnLoseFocus(); 17 | 18 | /* 19 | 20 | ViewRootImpl.java 21 | @Override 22 | public ViewParent getParent() { 23 | return null; 24 | } 25 | 26 | */ 27 | default void attachToColumnLayout(View me) { 28 | ViewParent parent = me.getParent(); 29 | if (parent instanceof ColumnLayout) { 30 | ColumnLayout columnLayout = (ColumnLayout) parent; 31 | columnLayout.addColumnFocusChangeListener(this); 32 | } else { 33 | while (parent != null) { 34 | parent = parent.getParent(); 35 | if (parent instanceof ColumnLayout) { 36 | break; 37 | } 38 | } 39 | if (parent != null) { 40 | ColumnLayout columnLayout = (ColumnLayout) parent; 41 | columnLayout.addColumnFocusChangeListener(this); 42 | } 43 | } 44 | } 45 | 46 | default void detachFromColumnLayout(View me) { 47 | ViewParent parent = me.getParent(); 48 | if (parent instanceof ColumnLayout) { 49 | ColumnLayout columnLayout = (ColumnLayout) parent; 50 | columnLayout.removeColumnFocusChangeListener(this); 51 | } else { 52 | while (parent != null) { 53 | parent = parent.getParent(); 54 | if (parent instanceof ColumnLayout) { 55 | break; 56 | } 57 | } 58 | if (parent != null) { 59 | ColumnLayout columnLayout = (ColumnLayout) parent; 60 | columnLayout.removeColumnFocusChangeListener(this); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/view/ColumnFocusChangeListenerTextView.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Color; 5 | import android.text.TextUtils; 6 | import android.util.AttributeSet; 7 | import android.widget.TextView; 8 | 9 | import androidx.annotation.Nullable; 10 | import androidx.appcompat.widget.AppCompatTextView; 11 | 12 | import com.msisuzney.tv.waterfallayout.view.ColumnFocusChangeListener; 13 | 14 | /** 15 | * @author: chenxin 16 | * @date: 2020-02-29 17 | * @email: chenxin7930@qq.com 18 | */ 19 | public class ColumnFocusChangeListenerTextView extends AppCompatTextView implements ColumnFocusChangeListener { 20 | public ColumnFocusChangeListenerTextView(Context context) { 21 | super(context); 22 | } 23 | 24 | public ColumnFocusChangeListenerTextView(Context context, @Nullable AttributeSet attrs) { 25 | super(context, attrs); 26 | } 27 | 28 | public ColumnFocusChangeListenerTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 29 | super(context, attrs, defStyleAttr); 30 | } 31 | 32 | @Override 33 | public void onColumnGainFocus() { 34 | setText("the column has gained focus"); 35 | textViewMarquee(this, true); 36 | } 37 | 38 | @Override 39 | public void onColumnLoseFocus() { 40 | setText("the column lost focus"); 41 | textViewMarquee(this, false); 42 | } 43 | 44 | @Override 45 | protected void onAttachedToWindow() { 46 | super.onAttachedToWindow(); 47 | attachToColumnLayout(this); 48 | } 49 | 50 | @Override 51 | protected void onDetachedFromWindow() { 52 | super.onDetachedFromWindow(); 53 | detachFromColumnLayout(this); 54 | } 55 | 56 | static void textViewMarquee(TextView view, boolean enable) { 57 | if (enable) { 58 | view.setMaxLines(1); 59 | view.setTextColor(Color.RED); 60 | view.setEllipsize(TextUtils.TruncateAt.MARQUEE); 61 | view.setMarqueeRepeatLimit(-1); 62 | view.setSelected(true); 63 | } else { 64 | view.setMaxLines(1); 65 | view.setEllipsize(TextUtils.TruncateAt.END); 66 | view.setTextColor(Color.BLACK); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/viewfactory/ColumnItemViewFactory.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.viewfactory; 2 | 3 | import com.msisuzney.tv.demo.MyStateChangeObservable; 4 | import com.msisuzney.tv.demo.bean.ColumnFocusStateBean; 5 | import com.msisuzney.tv.demo.bean.RecyclerViewStateBean; 6 | import com.msisuzney.tv.demo.bean.TabBean; 7 | import com.msisuzney.tv.demo.viewfactory.presenter.ColumnFocusChangeListenerTextViewPresenter; 8 | import com.msisuzney.tv.demo.viewfactory.presenter.ImageViewPresenter; 9 | import com.msisuzney.tv.demo.viewfactory.presenter.ImageViewPresenter2; 10 | import com.msisuzney.tv.demo.viewfactory.presenter.StateTextViewPresenter; 11 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 12 | import com.msisuzney.tv.waterfallayout.leanback.PresenterSelector; 13 | 14 | import com.msisuzney.tv.waterfallayout.OnItemKeyListener; 15 | 16 | /** 17 | * @author: chenxin 18 | * @date: 2019-12-20 19 | * @email: chenxin7930@qq.com 20 | */ 21 | 22 | /** 23 | * 行中的运营位的选择器 24 | */ 25 | public class ColumnItemViewFactory extends PresenterSelector { 26 | 27 | private ImageViewPresenter imageViewPresenter; 28 | private ImageViewPresenter2 imageViewPresenter2; 29 | private StateTextViewPresenter stateTextViewPresenter; 30 | 31 | private ColumnFocusChangeListenerTextViewPresenter changeListenerTextViewPresenter; 32 | 33 | public ColumnItemViewFactory(MyStateChangeObservable observable, 34 | OnItemKeyListener onItemKeyListener) { 35 | imageViewPresenter = new ImageViewPresenter(onItemKeyListener); 36 | imageViewPresenter2 = new ImageViewPresenter2(onItemKeyListener); 37 | stateTextViewPresenter = new StateTextViewPresenter(observable); 38 | changeListenerTextViewPresenter = new ColumnFocusChangeListenerTextViewPresenter(); 39 | } 40 | 41 | @Override 42 | public Presenter getPresenter(Object item) { 43 | if (item instanceof TabBean.ResultBean.AbsLayoutListBean) { 44 | return imageViewPresenter; 45 | } else if (item instanceof TabBean.ResultBean.HorizontalLayoutListBean) { 46 | return imageViewPresenter2; 47 | } else if (item instanceof RecyclerViewStateBean) { 48 | return stateTextViewPresenter; 49 | } else if (item instanceof ColumnFocusStateBean) { 50 | return changeListenerTextViewPresenter; 51 | } 52 | return null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/StateChangeObservable.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout; 2 | 3 | import androidx.recyclerview.widget.RecyclerView; 4 | 5 | import java.util.ArrayList; 6 | /** 7 | * @author: chenxin 8 | * @date: 2019-12-20 9 | * @email: chenxin7930@qq.com 10 | */ 11 | 12 | /** 13 | * 状态类,这里只定义了三种基础的状态,可以继承该类定义更多的状态 14 | * 15 | * @param 16 | */ 17 | public abstract class StateChangeObservable { 18 | 19 | void notifyFragmentPause() { 20 | for (int i = mObservers.size() - 1; i >= 0; i--) { 21 | mObservers.get(i).onFragmentPause(); 22 | } 23 | } 24 | 25 | void notifyFragmentVisibilityChanged(boolean isVisible) { 26 | for (int i = mObservers.size() - 1; i >= 0; i--) { 27 | mObservers.get(i).onFragmentVisibilityChanged(isVisible); 28 | } 29 | } 30 | 31 | void onScrollStateChanged(RecyclerView recyclerView, int newState) { 32 | for (int i = mObservers.size() - 1; i >= 0; i--) { 33 | mObservers.get(i).onScrollStateChanged(recyclerView, newState); 34 | } 35 | } 36 | 37 | /** 38 | * The list of observers. An observer can be in the list at most 39 | * once and will never be null. 40 | */ 41 | protected final ArrayList mObservers = new ArrayList(); 42 | 43 | 44 | public void registerObserver(T observer) { 45 | if (observer == null) { 46 | throw new IllegalArgumentException("The observer is null."); 47 | } 48 | synchronized (mObservers) { 49 | if (mObservers.contains(observer)) { 50 | throw new IllegalStateException("Observer " + observer + " is already registered."); 51 | } 52 | mObservers.add(observer); 53 | } 54 | } 55 | 56 | public void unregisterObserver(T observer) { 57 | if (observer == null) { 58 | throw new IllegalArgumentException("The observer is null."); 59 | } 60 | synchronized (mObservers) { 61 | int index = mObservers.indexOf(observer); 62 | if (index == -1) { 63 | throw new IllegalStateException("Observer " + observer + " was not registered."); 64 | } 65 | mObservers.remove(index); 66 | } 67 | } 68 | 69 | /** 70 | * Remove all registered observers. 71 | */ 72 | public void unregisterAll() { 73 | synchronized (mObservers) { 74 | mObservers.clear(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/view/ColumnLayout.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.view.ViewTreeObserver; 7 | import android.widget.AbsoluteLayout; 8 | 9 | import java.util.ArrayList; 10 | 11 | /** 12 | * @author: chenxin 13 | * @date: 2020-01-06 14 | */ 15 | public class ColumnLayout extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener { 16 | public ColumnLayout(Context context) { 17 | super(context); 18 | } 19 | 20 | public ColumnLayout(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | public ColumnLayout(Context context, AttributeSet attrs, int defStyleAttr) { 25 | super(context, attrs, defStyleAttr); 26 | } 27 | 28 | @Override 29 | protected void onAttachedToWindow() { 30 | super.onAttachedToWindow(); 31 | getRootView().getViewTreeObserver().addOnGlobalFocusChangeListener(this); 32 | } 33 | 34 | private ArrayList registeredFocusChangedViews = new ArrayList<>(); 35 | 36 | @Override 37 | protected void onDetachedFromWindow() { 38 | super.onDetachedFromWindow(); 39 | getRootView().getViewTreeObserver().removeOnGlobalFocusChangeListener(this); 40 | registeredFocusChangedViews.clear(); 41 | } 42 | 43 | void addColumnFocusChangeListener(ColumnFocusChangeListener childView) { 44 | registeredFocusChangedViews.add(childView); 45 | } 46 | 47 | void removeColumnFocusChangeListener(ColumnFocusChangeListener childView) { 48 | registeredFocusChangedViews.remove(childView); 49 | } 50 | 51 | private boolean hasColumnFocus = false; 52 | 53 | @Override 54 | public void onGlobalFocusChanged(View oldFocus, View newFocus) { 55 | if (registeredFocusChangedViews.size() == 0) return; 56 | if (hasFocus()) { 57 | if (!hasColumnFocus) { 58 | hasColumnFocus = true; 59 | for (ColumnFocusChangeListener view : registeredFocusChangedViews) { 60 | view.onColumnGainFocus(); 61 | } 62 | } 63 | } else { 64 | if (hasColumnFocus) { 65 | hasColumnFocus = false; 66 | for (ColumnFocusChangeListener view : registeredFocusChangedViews) { 67 | view.onColumnLoseFocus(); 68 | } 69 | } 70 | 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/ItemAlignment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.msisuzney.tv.waterfallayout.leanback; 16 | 17 | import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL; 18 | import static androidx.recyclerview.widget.RecyclerView.VERTICAL; 19 | 20 | import android.view.View; 21 | 22 | /** 23 | * Defines alignment position on two directions of an item view. Typically item 24 | * view alignment is at the center of the view. The class allows defining 25 | * alignment at left/right or fixed offset/percentage position; it also allows 26 | * using descendant view by id match. 27 | */ 28 | class ItemAlignment { 29 | 30 | final static class Axis extends ItemAlignmentFacet.ItemAlignmentDef { 31 | private int mOrientation; 32 | 33 | Axis(int orientation) { 34 | mOrientation = orientation; 35 | } 36 | 37 | /** 38 | * get alignment position relative to optical left/top of itemView. 39 | */ 40 | public int getAlignmentPosition(View itemView) { 41 | return ItemAlignmentFacetHelper.getAlignmentPosition(itemView, this, mOrientation); 42 | } 43 | } 44 | 45 | private int mOrientation = HORIZONTAL; 46 | 47 | final public Axis vertical = new Axis(VERTICAL); 48 | 49 | final public Axis horizontal = new Axis(HORIZONTAL); 50 | 51 | private Axis mMainAxis = horizontal; 52 | 53 | private Axis mSecondAxis = vertical; 54 | 55 | final public Axis mainAxis() { 56 | return mMainAxis; 57 | } 58 | 59 | final public Axis secondAxis() { 60 | return mSecondAxis; 61 | } 62 | 63 | final public void setOrientation(int orientation) { 64 | mOrientation = orientation; 65 | if (mOrientation == HORIZONTAL) { 66 | mMainAxis = horizontal; 67 | mSecondAxis = vertical; 68 | } else { 69 | mMainAxis = vertical; 70 | mSecondAxis = horizontal; 71 | } 72 | } 73 | 74 | final public int getOrientation() { 75 | return mOrientation; 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/OnChildViewHolderSelectedListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import com.msisuzney.tv.waterfallayout.leanback.ItemAlignmentFacet.ItemAlignmentDef; 17 | import androidx.recyclerview.widget.RecyclerView; 18 | 19 | /** 20 | * Interface for receiving notification when a child of this ViewGroup has been selected. 21 | * There are two methods: 22 | *
  • 23 | * {link {@link #onChildViewHolderSelected(RecyclerView, RecyclerView.ViewHolder, int, int)}} 24 | * is called when the view holder is about to be selected. The listener could change size 25 | * of the view holder in this callback. 26 | *
  • 27 | *
  • 28 | * {link {@link #onChildViewHolderSelectedAndPositioned(RecyclerView, RecyclerView.ViewHolder, 29 | * int, int)} is called when view holder has been selected and laid out in RecyclerView. 30 | * 31 | *
  • 32 | */ 33 | public abstract class OnChildViewHolderSelectedListener { 34 | /** 35 | * Callback method to be invoked when a child of this ViewGroup has been selected. Listener 36 | * might change the size of the child and the position of the child is not finalized. To get 37 | * the final layout position of child, overide {@link #onChildViewHolderSelectedAndPositioned( 38 | * RecyclerView, RecyclerView.ViewHolder, int, int)}. 39 | * 40 | * @param parent The RecyclerView where the selection happened. 41 | * @param child The ViewHolder within the RecyclerView that is selected, or null if no 42 | * view is selected. 43 | * @param position The position of the view in the adapter, or NO_POSITION 44 | * if no view is selected. 45 | * @param subposition The index of which {@link ItemAlignmentDef} being used, 46 | * 0 if there is no ItemAlignmentDef defined for the item. 47 | */ 48 | public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, 49 | int position, int subposition) { 50 | } 51 | 52 | /** 53 | * Callback method to be invoked when a child of this ViewGroup has been selected and 54 | * positioned. 55 | * 56 | * @param parent The RecyclerView where the selection happened. 57 | * @param child The ViewHolder within the RecyclerView that is selected, or null if no 58 | * view is selected. 59 | * @param position The position of the view in the adapter, or NO_POSITION 60 | * if no view is selected. 61 | * @param subposition The index of which {@link ItemAlignmentDef} being used, 62 | * 0 if there is no ItemAlignmentDef defined for the item. 63 | */ 64 | public void onChildViewHolderSelectedAndPositioned(RecyclerView parent, 65 | RecyclerView.ViewHolder child, int position, int subposition) { 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/presenter/ColumnLayoutRowPresenter.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.presenter; 2 | 3 | import com.msisuzney.tv.waterfallayout.R; 4 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 5 | import com.msisuzney.tv.waterfallayout.leanback.PresenterSelector; 6 | import com.msisuzney.tv.waterfallayout.model.ColumnLayoutCollection; 7 | import com.msisuzney.tv.waterfallayout.model.ColumnLayoutItem; 8 | import com.msisuzney.tv.waterfallayout.view.ColumnLayout; 9 | 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.widget.AbsoluteLayout; 13 | 14 | import java.util.List; 15 | 16 | 17 | class ColumnLayoutRowPresenter extends Presenter { 18 | private PresenterSelector blockPresenterSelector; 19 | 20 | 21 | ColumnLayoutRowPresenter(PresenterSelector blockPresenterSelector) { 22 | this.blockPresenterSelector = blockPresenterSelector; 23 | } 24 | 25 | @Override 26 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 27 | View view = new ColumnLayout(parent.getContext()); 28 | return new ColumnViewHolder(view); 29 | } 30 | 31 | 32 | @Override 33 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 34 | ColumnLayoutCollection collection = (ColumnLayoutCollection) item; 35 | ColumnViewHolder columnViewHolder = (ColumnViewHolder) viewHolder; 36 | AbsoluteLayout parentView = columnViewHolder.getAbsoluteLayout(); 37 | ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(collection.getWidth(), collection.getHeight()); 38 | parentView.setLayoutParams(lp); 39 | 40 | 41 | List items = collection.getItems(); 42 | if (items != null) { 43 | for (ColumnLayoutItem layoutItem : items) { 44 | Presenter presenter = blockPresenterSelector.getPresenter(layoutItem.getData()); 45 | if (presenter != null) { 46 | ViewHolder vh = presenter.onCreateViewHolder(parentView); 47 | vh.view.setTag(R.id.lb_view_data_tag, layoutItem.getData()); 48 | vh.view.setTag(R.id.lb_view_holder_tag, vh); 49 | presenter.onBindViewHolder(vh, layoutItem.getData()); 50 | AbsoluteLayout.LayoutParams layoutParams = new AbsoluteLayout.LayoutParams(layoutItem.getWidth(), 51 | layoutItem.getHeight(), layoutItem.getX(), layoutItem.getY()); 52 | parentView.addView(vh.view, layoutParams); 53 | } 54 | } 55 | } 56 | } 57 | 58 | //被RV回收时调用 59 | @Override 60 | public void onUnbindViewHolder(ViewHolder viewHolder) { 61 | ColumnViewHolder columnViewHolder = (ColumnViewHolder) viewHolder; 62 | AbsoluteLayout parentView = columnViewHolder.getAbsoluteLayout(); 63 | for (int i = 0; i < parentView.getChildCount(); i++) { 64 | View view = parentView.getChildAt(i); 65 | Object data = view.getTag(R.id.lb_view_data_tag); 66 | ViewHolder vh = (ViewHolder) view.getTag(R.id.lb_view_holder_tag); 67 | Presenter presenter = blockPresenterSelector.getPresenter(data); 68 | presenter.onUnbindViewHolder(vh); 69 | view.setTag(R.id.lb_view_holder_tag, null); 70 | view.setTag(R.id.lb_view_data_tag, null); 71 | } 72 | parentView.removeAllViews(); 73 | } 74 | 75 | 76 | public final class ColumnViewHolder extends ViewHolder { 77 | 78 | private AbsoluteLayout absoluteLayout; 79 | 80 | public ColumnViewHolder(View view) { 81 | super(view); 82 | absoluteLayout = (AbsoluteLayout) view; 83 | } 84 | 85 | public AbsoluteLayout getAbsoluteLayout() { 86 | return absoluteLayout; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/VerticalGridView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import android.content.Context; 17 | import android.content.res.TypedArray; 18 | import com.msisuzney.tv.waterfallayout.R; 19 | import androidx.recyclerview.widget.RecyclerView; 20 | import android.util.AttributeSet; 21 | import android.util.TypedValue; 22 | 23 | /** 24 | * A {@link android.view.ViewGroup} that shows items in a vertically scrolling list. The items 25 | * come from the {@link RecyclerView.Adapter} associated with this view. 26 | *

    27 | * {@link RecyclerView.Adapter} can optionally implement {@link FacetProviderAdapter} which 28 | * provides {@link FacetProvider} for a given view type; {@link RecyclerView.ViewHolder} 29 | * can also implement {@link FacetProvider}. Facet from ViewHolder 30 | * has a higher priority than the one from FacetProviderAdapter associated with viewType. 31 | * Supported optional facets are: 32 | *

      33 | *
    1. {@link ItemAlignmentFacet} 34 | * When this facet is provided by ViewHolder or FacetProviderAdapter, it will 35 | * override the item alignment settings set on VerticalGridView. This facet also allows multiple 36 | * alignment positions within one ViewHolder. 37 | *
    2. 38 | *
    39 | */ 40 | public class VerticalGridView extends BaseGridView { 41 | 42 | public VerticalGridView(Context context) { 43 | this(context, null); 44 | } 45 | 46 | public VerticalGridView(Context context, AttributeSet attrs) { 47 | this(context, attrs, 0); 48 | } 49 | 50 | public VerticalGridView(Context context, AttributeSet attrs, int defStyle) { 51 | super(context, attrs, defStyle); 52 | mLayoutManager.setOrientation(RecyclerView.VERTICAL); 53 | initAttributes(context, attrs); 54 | } 55 | 56 | protected void initAttributes(Context context, AttributeSet attrs) { 57 | initBaseGridViewAttributes(context, attrs); 58 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbVerticalGridView); 59 | setColumnWidth(a); 60 | setNumColumns(a.getInt(R.styleable.lbVerticalGridView_numberOfColumns, 1)); 61 | a.recycle(); 62 | } 63 | 64 | void setColumnWidth(TypedArray array) { 65 | TypedValue typedValue = array.peekValue(R.styleable.lbVerticalGridView_columnWidth); 66 | if (typedValue != null) { 67 | int size = array.getLayoutDimension(R.styleable.lbVerticalGridView_columnWidth, 0); 68 | setColumnWidth(size); 69 | } 70 | } 71 | 72 | /** 73 | * Sets the number of columns. Defaults to one. 74 | */ 75 | public void setNumColumns(int numColumns) { 76 | mLayoutManager.setNumRows(numColumns); 77 | requestLayout(); 78 | } 79 | 80 | /** 81 | * Sets the column width. 82 | * 83 | * @param width May be {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}, or a size 84 | * in pixels. If zero, column width will be fixed based on number of columns 85 | * and view width. 86 | */ 87 | public void setColumnWidth(int width) { 88 | mLayoutManager.setRowHeight(width); 89 | requestLayout(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android TV瀑布流控件 2 | **一种基于Android Leanback改造的Android TV瀑布流布局** 3 | 4 | 示例效果如下: 5 |
    6 | 演示 7 |
    8 | 9 | ### 解决的问题 10 | 瀑布流布局的运营按照栏目划分,每个栏目中的资源、海报宽高完全按运营需要自定义, 11 | 但Leanback不支持自定义栏目中的View的宽高、栏目中的View居中 12 | ### 特性 13 | 14 | - 以`行`做为瀑布流布局的运营单元,行的布局可以是`HorizontalGridView`或者`ColumnLayout`,也可以自定义`行`的布局 15 | - 获得焦点的`View`自动居中显示 16 | - 快速滑动时不会出现焦点移动不合理的情况 17 | - 支持焦点自动换行,使用`FocusLineFeedFrameLayout`作为子View的根布局,当焦点在屏幕右边缘时按下右键,焦点会换行到下一行的第一个`View`,左边缘同理换行到上一行最后一个`View` 18 | - `ColumnLayout`栏目中的子`View`可以实现`ColumnFocusChangeListener`监听整个栏目的焦点变化 19 | - 栏目中的`View`可以通过实现`StateChangedObserver`接口,并将自己注册给`StateChangeObservable`,以便监听`RecyclerView`的滑动状态 20 | 21 | 以上所有特性都在demo中演示 22 | ### 使用 23 | #### 1.设计理念 24 | 延用了Leanback中的`Model -> Presenter -> View`的理念: 25 | 26 |
    27 | 演示 28 |
    29 | 30 | Presenters根据不同的Bean创建不同的View,具体见[android/tv-samples](https://github.com/android/tv-samples) 31 | 32 | #### 2.使用方式 33 | 0. 添加依赖 34 | ```gradle 35 | //1) 根目录下build.gradle 36 | allprojects { 37 | repositories { 38 | //add jitpack.io repo 39 | maven { url 'https://jitpack.io' } 40 | } 41 | } 42 | 43 | //2) module build.gradle 44 | dependencies { 45 | //add lib 46 | implementation 'com.github.msisuzney:tv-waterfall-layout:1.0.1' 47 | } 48 | ``` 49 | 1. 继承`RowsFragment` 50 | 2. 添加`ColumnLayout`布局栏目,使用`ColumnLayoutCollection`定义栏目的宽高,再使用`ColumnLayoutItem`定义子`View`的位置、大小、bean类型与数据, 51 | 最后使用`setItems`方法将`ColumnLayoutItems`添加到`ColumnLayoutCollection`中 52 | 3. 添加水平滑动的`HorizontalGirdView`布局栏目,使用`HorizontalLayoutCollection`定义栏目的宽高,再使用`HorizontalLayoutItem`定义子`View`的大小、bean类型与数据, 53 | 最后使用`setItems`方法将`HorizontalLayoutItems`添加到`HorizontalLayoutCollection` 54 | 4. 使用`RowsFragment`#`add`方法将`ColumnLayoutCollection`/`HorizontalLayoutCollection`添加到布局中 55 | 5. 复写`RowsFragment`#`initBlockPresenterSelector`方法,返回的`PresenterSelector`用于根据bean类型为栏目创建不同的`View` 56 | 57 | 详细使用见demo module代码,简易代码如下: 58 | ```java 59 | public class MyFragment extends RowsFragment { 60 | 61 | 62 | @Override 63 | protected PresenterSelector initBlockPresenterSelector() { 64 | //1.提供所有行中的运营位的Presenters,用于创建对应的View 65 | return new PresenterSelector() { 66 | @Override 67 | public Presenter getPresenter(Object item) { 68 | return new ImageViewPresenter(null); 69 | } 70 | }; 71 | } 72 | 73 | @Override 74 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 75 | super.onViewCreated(view, savedInstanceState); 76 | //2. 构造水平滑动布局的Model 77 | HorizontalLayoutItem item = new HorizontalLayoutItem(); 78 | item.setWidth(200); 79 | item.setHeight(200); 80 | List items = new ArrayList<>(); 81 | items.add(item); 82 | HorizontalLayoutCollection horizontalLayoutCollection = 83 | new HorizontalLayoutCollection(ViewGroup.LayoutParams.MATCH_PARENT, 200); 84 | horizontalLayoutCollection.setItems(items); 85 | 86 | //3. 构造绝对布局的Model 87 | AbsoluteLayoutItem item1 = new AbsoluteLayoutItem(); 88 | item1.setHeight(200); 89 | item1.setWidth(200); 90 | item1.setX(200); 91 | item1.setY(10); 92 | List items1 = new ArrayList<>(); 93 | items1.add(item1); 94 | AbsoluteLayoutCollection absoluteLayoutCollection = 95 | new AbsoluteLayoutCollection(ViewGroup.LayoutParams.MATCH_PARENT, 400); 96 | absoluteLayoutCollection.setItems(items1); 97 | 98 | //4. 添加到布局中 99 | add(horizontalLayoutCollection); 100 | add(absoluteLayoutCollection); 101 | } 102 | } 103 | 104 | 105 | ``` 106 | 107 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | /** 16 | *

    Support classes providing low level Leanback user interface building blocks: 17 | * widgets and helpers.

    18 | *

    19 | * The core interface to the developer’s model is the 20 | * {@link com.msisuzney.tv.waterfallayout.leanback.ObjectAdapter}. It is similar to Adapter and the 21 | * RecyclerView Adapter, but separates iterating items from presenting them as Views. 22 | * Concrete implementations include 23 | * {@link com.msisuzney.tv.waterfallayout.leanback.ArrayObjectAdapter} and 24 | * {@link androidx.leanback.widget.CursorObjectAdapter}, but a developer is free to use a 25 | * subclass of an ObjectAdapter to iterate over any existing Object hierarchy. 26 | *

    27 | *

    28 | * A {@link com.msisuzney.tv.waterfallayout.leanback.Presenter} creates Views and binds data from an Object 29 | * to those Views. This is the 30 | * complementary piece to ObjectAdapter that corresponds to existing Android adapter classes. 31 | * The benefit to separating out a Presenter is that we can use it to generate Views outside of the 32 | * context of an adapter. For example, a UI may represent data from a single Object in several places 33 | * at once. Each View that needs to be generated can be produced by a different Presenter, while the 34 | * Object is retrieved from the ObjectAdapter once. 35 | *

    36 | * A {@link com.msisuzney.tv.waterfallayout.leanback.PresenterSelector} determines which Presenter to use 37 | * for a given Object from an 38 | * ObjectAdapter. Two common cases are when an ObjectAdapter uses the same View type for every element 39 | * ({@link com.msisuzney.tv.waterfallayout.leanback.SinglePresenterSelector}), and when the Presenter is 40 | * determined by the Java class of 41 | * the element ({@link androidx.leanback.widget.ClassPresenterSelector}). A developer is 42 | * able to implement any selection logic 43 | * as a PresenterSelector. For example, if all the elements of an ObjectAdapter have the same type, 44 | * but certain elements are to be rendered using a 'promotional content' view in the developer’s 45 | * application, the PresenterSelector may inspect the fields of each element before choosing the 46 | * appropriate Presenter. 47 | *

    48 | *

    49 | * The basic navigation model for Leanback is that of a vertical list of rows, each of which may 50 | * be a horizontal list of items. Therefore, Leanback uses ObjectAdapters both for defining the 51 | * horizontal data items as well as the list of rows themselves. 52 | *

    53 | *

    Leanback defines a few basic data model classes for rows: the 54 | * {@link androidx.leanback.widget.Row}, which defines the 55 | * abstract concept of a row with a header; and {@link androidx.leanback.widget.ListRow}, 56 | * a concrete Row implementation that uses an ObjectAdapter to present a horizontal list of items. 57 | * The corresponding presenter for the ListRow is the 58 | * {@link androidx.leanback.widget.ListRowPresenter}. 59 | *

    60 | *

    61 | * Other types of Rows and corresponding RowPresenters are provided; however the application may 62 | * define a custom subclass of {@link androidx.leanback.widget.Row} and 63 | * {@link androidx.leanback.widget.RowPresenter}. 64 | *

    65 | */ 66 | 67 | package com.msisuzney.tv.waterfallayout.leanback; 68 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/ItemAlignmentFacetHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import static com.msisuzney.tv.waterfallayout.leanback.ItemAlignmentFacet.ITEM_ALIGN_OFFSET_PERCENT_DISABLED; 17 | import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL; 18 | 19 | import android.graphics.Paint; 20 | import android.graphics.Rect; 21 | import com.msisuzney.tv.waterfallayout.leanback.GridLayoutManager.LayoutParams; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.TextView; 25 | 26 | /** 27 | * Helper class to handle ItemAlignmentFacet in a grid view. 28 | */ 29 | class ItemAlignmentFacetHelper { 30 | 31 | private static Rect sRect = new Rect(); 32 | 33 | /** 34 | * get alignment position relative to optical left/top of itemView. 35 | */ 36 | static int getAlignmentPosition(View itemView, ItemAlignmentFacet.ItemAlignmentDef facet, 37 | int orientation) { 38 | LayoutParams p = (LayoutParams) itemView.getLayoutParams(); 39 | View view = itemView; 40 | if (facet.mViewId != 0) { 41 | view = itemView.findViewById(facet.mViewId); 42 | if (view == null) { 43 | view = itemView; 44 | } 45 | } 46 | int alignPos = facet.mOffset; 47 | if (orientation == HORIZONTAL) { 48 | if (facet.mOffset >= 0) { 49 | if (facet.mOffsetWithPadding) { 50 | alignPos += view.getPaddingLeft(); 51 | } 52 | } else { 53 | if (facet.mOffsetWithPadding) { 54 | alignPos -= view.getPaddingRight(); 55 | } 56 | } 57 | if (facet.mOffsetPercent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) { 58 | alignPos += ((view == itemView ? p.getOpticalWidth(view) : view.getWidth()) 59 | * facet.mOffsetPercent) / 100f; 60 | } 61 | if (itemView != view) { 62 | sRect.left = alignPos; 63 | ((ViewGroup) itemView).offsetDescendantRectToMyCoords(view, sRect); 64 | alignPos = sRect.left - p.getOpticalLeftInset(); 65 | } 66 | } else { 67 | if (facet.mOffset >= 0) { 68 | if (facet.mOffsetWithPadding) { 69 | alignPos += view.getPaddingTop(); 70 | } 71 | } else { 72 | if (facet.mOffsetWithPadding) { 73 | alignPos -= view.getPaddingBottom(); 74 | } 75 | } 76 | if (facet.mOffsetPercent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) { 77 | alignPos += ((view == itemView ? p.getOpticalHeight(view) : view.getHeight()) 78 | * facet.mOffsetPercent) / 100f; 79 | } 80 | if (itemView != view) { 81 | sRect.top = alignPos; 82 | ((ViewGroup) itemView).offsetDescendantRectToMyCoords(view, sRect); 83 | alignPos = sRect.top - p.getOpticalTopInset(); 84 | } 85 | if (view instanceof TextView && facet.isAlignedToTextViewBaseLine()) { 86 | Paint textPaint = ((TextView)view).getPaint(); 87 | int titleViewTextHeight = -textPaint.getFontMetricsInt().top; 88 | alignPos += titleViewTextHeight; 89 | } 90 | } 91 | return alignPos; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/viewfactory/presenter/ImageViewPresenter.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.viewfactory.presenter; 2 | 3 | import android.graphics.Color; 4 | 5 | import com.msisuzney.tv.demo.R; 6 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.view.animation.Interpolator; 11 | import android.view.animation.OvershootInterpolator; 12 | import android.widget.FrameLayout; 13 | import android.widget.ImageView; 14 | 15 | 16 | import com.bumptech.glide.Glide; 17 | import com.msisuzney.tv.demo.WaterfallFragment; 18 | import com.msisuzney.tv.waterfallayout.OnItemKeyListener; 19 | /** 20 | * @author: chenxin 21 | * @date: 2019-12-20 22 | * @email: chenxin7930@qq.com 23 | */ 24 | public class ImageViewPresenter extends Presenter { 25 | 26 | 27 | private static Interpolator sOvershootInterpolator = new OvershootInterpolator(); 28 | 29 | private OnItemKeyListener onItemKeyListener; 30 | 31 | public ImageViewPresenter(OnItemKeyListener onItemKeyListener) { 32 | this.onItemKeyListener = onItemKeyListener; 33 | } 34 | 35 | @Override 36 | public MyViewHolder onCreateViewHolder(ViewGroup parent) { 37 | 38 | return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate( 39 | R.layout.block_image, parent, false)); 40 | } 41 | 42 | @Override 43 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 44 | if (viewHolder instanceof MyViewHolder) { 45 | MyViewHolder vh = (MyViewHolder) viewHolder; 46 | View containerView = vh.view; 47 | final ImageView imageView = vh.imageView; 48 | final FrameLayout imageDock = vh.imageDock; 49 | FrameLayout.LayoutParams imageDockLp = (FrameLayout.LayoutParams) imageDock.getLayoutParams(); 50 | imageDockLp.setMargins(WaterfallFragment.COLUMN_ITEM_PADDING, WaterfallFragment.COLUMN_ITEM_PADDING, 51 | WaterfallFragment.COLUMN_ITEM_PADDING, WaterfallFragment.COLUMN_ITEM_PADDING); 52 | imageDock.setLayoutParams(imageDockLp); 53 | 54 | containerView.setOnFocusChangeListener((v, hasFocus) -> { 55 | if (hasFocus) { 56 | imageDock.setBackgroundColor(Color.RED); 57 | v.animate().scaleX(1.2f).scaleY(1.2f).setInterpolator(sOvershootInterpolator).setDuration(150).start(); 58 | v.bringToFront(); 59 | } else { 60 | imageDock.setBackgroundResource(0); 61 | v.animate().scaleX(1.0f).scaleY(1.0f).setDuration(150).start(); 62 | } 63 | }); 64 | containerView.setOnKeyListener((v, keyCode, event) -> { 65 | if (onItemKeyListener != null && onItemKeyListener.onKey(v, event, item)) { 66 | return true; 67 | } 68 | return false; 69 | }); 70 | 71 | if (onItemKeyListener != null) { 72 | containerView.setOnClickListener(v -> { 73 | onItemKeyListener.onClick(item); 74 | }); 75 | } 76 | Glide.with(imageView) 77 | .load(R.drawable.tu) 78 | .into(imageView); 79 | } 80 | } 81 | 82 | 83 | @Override 84 | public void onUnbindViewHolder(ViewHolder viewHolder) { 85 | if (viewHolder instanceof MyViewHolder) { 86 | MyViewHolder myViewHolder = (MyViewHolder) viewHolder; 87 | myViewHolder.view.setOnKeyListener(null); 88 | myViewHolder.view.setOnFocusChangeListener(null); 89 | Glide.with(myViewHolder.imageView).clear(myViewHolder.imageView); 90 | } 91 | } 92 | 93 | 94 | public static class MyViewHolder extends ViewHolder { 95 | 96 | public ImageView imageView; 97 | public FrameLayout imageDock; 98 | 99 | public MyViewHolder(View view) { 100 | super(view); 101 | imageView = view.findViewById(R.id.image); 102 | imageDock = view.findViewById(R.id.image_dock); 103 | 104 | } 105 | } 106 | 107 | 108 | } 109 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/viewfactory/presenter/ImageViewPresenter2.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.viewfactory.presenter; 2 | 3 | import android.graphics.Color; 4 | 5 | import com.msisuzney.tv.demo.R; 6 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.view.animation.Interpolator; 11 | import android.view.animation.OvershootInterpolator; 12 | import android.widget.FrameLayout; 13 | import android.widget.ImageView; 14 | 15 | import com.bumptech.glide.Glide; 16 | import com.msisuzney.tv.demo.WaterfallFragment; 17 | import com.msisuzney.tv.demo.bean.TabBean; 18 | import com.msisuzney.tv.waterfallayout.OnItemKeyListener; 19 | /** 20 | * @author: chenxin 21 | * @date: 2019-12-20 22 | * @email: chenxin7930@qq.com 23 | */ 24 | public class ImageViewPresenter2 extends Presenter { 25 | 26 | 27 | private static Interpolator sOvershootInterpolator = new OvershootInterpolator(); 28 | 29 | private OnItemKeyListener onItemKeyListener; 30 | 31 | public ImageViewPresenter2(OnItemKeyListener onItemKeyListener) { 32 | this.onItemKeyListener = onItemKeyListener; 33 | } 34 | 35 | @Override 36 | public MyViewHolder onCreateViewHolder(ViewGroup parent) { 37 | return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate( 38 | R.layout.block_image, parent, false)); 39 | } 40 | 41 | @Override 42 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 43 | if (viewHolder instanceof MyViewHolder) { 44 | TabBean.ResultBean.HorizontalLayoutListBean bean = (TabBean.ResultBean.HorizontalLayoutListBean) item; 45 | MyViewHolder vh = (MyViewHolder) viewHolder; 46 | View containerView = vh.view; 47 | final ImageView imageView = vh.imageView; 48 | final FrameLayout imageDock = vh.imageDock; 49 | ViewGroup.LayoutParams vlp = vh.view.getLayoutParams(); 50 | vlp.height = ViewGroup.LayoutParams.MATCH_PARENT; 51 | vlp.width = ViewGroup.LayoutParams.MATCH_PARENT; 52 | vh.view.setLayoutParams(vlp); 53 | 54 | FrameLayout.LayoutParams imageDockLp = (FrameLayout.LayoutParams) imageDock.getLayoutParams(); 55 | imageDockLp.setMargins(WaterfallFragment.COLUMN_ITEM_PADDING, WaterfallFragment.COLUMN_ITEM_PADDING, 56 | WaterfallFragment.COLUMN_ITEM_PADDING, WaterfallFragment.COLUMN_ITEM_PADDING); 57 | imageDock.setLayoutParams(imageDockLp); 58 | 59 | containerView.setOnFocusChangeListener((v, hasFocus) -> { 60 | if (hasFocus) { 61 | imageDock.setBackgroundColor(Color.RED); 62 | v.animate().scaleX(1.2f).scaleY(1.2f).setInterpolator(sOvershootInterpolator).setDuration(150).start(); 63 | } else { 64 | imageDock.setBackgroundResource(0); 65 | v.animate().scaleX(1.0f).scaleY(1.0f).setDuration(150).start(); 66 | } 67 | }); 68 | containerView.setOnKeyListener((v, keyCode, event) -> { 69 | if (onItemKeyListener != null && onItemKeyListener.onKey(v, event, item)) { 70 | return true; 71 | } 72 | return false; 73 | }); 74 | 75 | if (onItemKeyListener != null) { 76 | containerView.setOnClickListener(v -> { 77 | onItemKeyListener.onClick(item); 78 | }); 79 | } 80 | Glide.with(imageView) 81 | .load(R.drawable.tu) 82 | .into(imageView); 83 | } 84 | } 85 | 86 | 87 | @Override 88 | public void onUnbindViewHolder(ViewHolder viewHolder) { 89 | if (viewHolder instanceof MyViewHolder) { 90 | MyViewHolder myViewHolder = (MyViewHolder) viewHolder; 91 | myViewHolder.view.setOnKeyListener(null); 92 | myViewHolder.view.setOnFocusChangeListener(null); 93 | Glide.with(myViewHolder.imageView).clear(myViewHolder.imageView); 94 | } 95 | } 96 | 97 | 98 | public static class MyViewHolder extends ViewHolder { 99 | 100 | public ImageView imageView; 101 | public FrameLayout imageDock; 102 | 103 | public MyViewHolder(View view) { 104 | super(view); 105 | imageView = view.findViewById(R.id.image); 106 | imageDock = view.findViewById(R.id.image_dock); 107 | 108 | } 109 | } 110 | 111 | 112 | } 113 | -------------------------------------------------------------------------------- /demo/src/main/assets/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "errcode": "0", 3 | "description": "成功执行", 4 | "result": [ 5 | { 6 | "columnTitle": "AbsoluteLayout的栏目,View的宽高自定", 7 | "columns": 28, 8 | "rows": 7, 9 | "absLayoutList": [ 10 | { 11 | "x": 0, 12 | "y": 0, 13 | "w": 9, 14 | "h": 7 15 | }, 16 | { 17 | "x": 9, 18 | "y": 0, 19 | "w": 15, 20 | "h": 7 21 | }, 22 | { 23 | "x": 24, 24 | "y": 0, 25 | "w": 4, 26 | "h": 7 27 | } 28 | ] 29 | }, 30 | { 31 | "columnTitle": "AbsoluteLayout的栏目,支持布局中的View自动居中", 32 | "columns": 40, 33 | "rows": 20, 34 | "absLayoutList": [ 35 | { 36 | "x": 0, 37 | "y": 0, 38 | "w": 10, 39 | "h": 20 40 | }, 41 | { 42 | "x": 10, 43 | "y": 0, 44 | "w": 10, 45 | "h": 10 46 | }, 47 | { 48 | "x": 10, 49 | "y": 10, 50 | "w": 10, 51 | "h": 10 52 | }, 53 | { 54 | "x": 20, 55 | "y": 0, 56 | "w": 10, 57 | "h": 12 58 | }, 59 | { 60 | "x": 20, 61 | "y": 12, 62 | "w": 10, 63 | "h": 8 64 | }, 65 | { 66 | "x": 30, 67 | "y": 0, 68 | "w": 10, 69 | "h": 8 70 | }, 71 | { 72 | "x": 30, 73 | "y": 8, 74 | "w": 10, 75 | "h": 8 76 | }, 77 | { 78 | "x": 30, 79 | "y": 16, 80 | "w": 10, 81 | "h": 4 82 | } 83 | ] 84 | }, 85 | { 86 | "columnTitle":"HorizontalGridView布局的栏目,View的宽高自定", 87 | "type": 1, 88 | "columns": 10, 89 | "rows": 2, 90 | "horizontalLayoutList": [ 91 | { 92 | "w": 3, 93 | "h": 2, 94 | "posterUrl": "" 95 | }, 96 | { 97 | "w": 4, 98 | "h": 2, 99 | "posterUrl": "" 100 | }, 101 | { 102 | "w": 5, 103 | "h": 2, 104 | "posterUrl": "" 105 | }, 106 | { 107 | "w": 2, 108 | "h": 2, 109 | "posterUrl": "" 110 | }, 111 | { 112 | "w": 2, 113 | "h": 2, 114 | "posterUrl": "" 115 | } 116 | ] 117 | }, 118 | { 119 | "columnTitle": "我是Title", 120 | "columns": 28, 121 | "rows": 7, 122 | "absLayoutList": [ 123 | { 124 | "posterUrl": "", 125 | "x": 0, 126 | "y": 0, 127 | "w": 14, 128 | "h": 7 129 | }, 130 | { 131 | "x": 14, 132 | "y": 0, 133 | "w": 7, 134 | "h": 7 135 | }, 136 | { 137 | "x": 21, 138 | "y": 0, 139 | "w": 7, 140 | "h": 7 141 | } 142 | ] 143 | }, 144 | { 145 | "columns": 10, 146 | "rows": 3, 147 | "absLayoutList": [ 148 | { 149 | "posterUrl": "", 150 | "x": 0, 151 | "y": 0, 152 | "w": 2, 153 | "h": 3 154 | }, 155 | { 156 | "x": 2, 157 | "y": 0, 158 | "w": 2, 159 | "h": 3 160 | }, 161 | { 162 | "x": 4, 163 | "y": 0, 164 | "w": 2, 165 | "h": 3 166 | }, 167 | { 168 | "x": 6, 169 | "y": 0, 170 | "w": 2, 171 | "h": 3 172 | }, 173 | { 174 | "x": 8, 175 | "y": 0, 176 | "w": 2, 177 | "h": 3 178 | } 179 | ] 180 | }, 181 | { 182 | "columns": 10, 183 | "rows": 2, 184 | "absLayoutList": [ 185 | { 186 | "posterUrl": "", 187 | "x": 0, 188 | "y": 0, 189 | "w": 10, 190 | "h": 2 191 | } 192 | ] 193 | }, 194 | { 195 | "columnTitle": "我是Title", 196 | "columns": 28, 197 | "rows": 7, 198 | "absLayoutList": [ 199 | { 200 | "posterUrl": "", 201 | "x": 0, 202 | "y": 0, 203 | "w": 14, 204 | "h": 7 205 | }, 206 | { 207 | "x": 14, 208 | "y": 0, 209 | "w": 7, 210 | "h": 7 211 | }, 212 | { 213 | "x": 21, 214 | "y": 0, 215 | "w": 7, 216 | "h": 7 217 | } 218 | ] 219 | } 220 | ] 221 | } -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/presenter/HorizontalLayoutRowPresenter.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.presenter; 2 | 3 | import com.msisuzney.tv.waterfallayout.R; 4 | import com.msisuzney.tv.waterfallayout.leanback.ArrayObjectAdapter; 5 | import com.msisuzney.tv.waterfallayout.leanback.HorizontalGridView; 6 | import com.msisuzney.tv.waterfallayout.leanback.ItemBridgeAdapter; 7 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 8 | import com.msisuzney.tv.waterfallayout.leanback.PresenterSelector; 9 | 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.FrameLayout; 14 | 15 | import com.msisuzney.tv.waterfallayout.model.HorizontalLayoutCollection; 16 | import com.msisuzney.tv.waterfallayout.model.HorizontalLayoutItem; 17 | /** 18 | * @author: chenxin 19 | * @date: 2019-12-20 20 | * @email: chenxin7930@qq.com 21 | */ 22 | class HorizontalLayoutRowPresenter extends Presenter { 23 | private Presenter itemDockPresenter; 24 | 25 | public HorizontalLayoutRowPresenter(PresenterSelector blockPresenterSelector) { 26 | itemDockPresenter = new Presenter() { 27 | @Override 28 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 29 | FrameLayout dockView = new FrameLayout(parent.getContext()); 30 | return new ViewHolder(dockView); 31 | } 32 | 33 | @Override 34 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 35 | HorizontalLayoutItem horizontalLayoutItem = (HorizontalLayoutItem) item; 36 | ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(horizontalLayoutItem.getWidth(), horizontalLayoutItem.getHeight()); 37 | viewHolder.view.setLayoutParams(lp); 38 | Presenter itemPresenter = blockPresenterSelector.getPresenter(horizontalLayoutItem.getData()); 39 | ViewHolder vh = itemPresenter.onCreateViewHolder((ViewGroup) viewHolder.view); 40 | itemPresenter.onBindViewHolder(vh, horizontalLayoutItem.getData()); 41 | viewHolder.view.setTag(R.id.lb_view_data_tag, horizontalLayoutItem.getData()); 42 | viewHolder.view.setTag(R.id.lb_view_holder_tag, viewHolder); 43 | ((ViewGroup) viewHolder.view).addView(vh.view); 44 | } 45 | 46 | @Override 47 | public void onUnbindViewHolder(ViewHolder viewHolder) { 48 | Object data = viewHolder.view.getTag(R.id.lb_view_data_tag); 49 | ViewHolder vh = (ViewHolder) viewHolder.view.getTag(R.id.lb_view_holder_tag); 50 | Presenter presenter = blockPresenterSelector.getPresenter(data); 51 | presenter.onUnbindViewHolder(vh); 52 | viewHolder.view.setTag(R.id.lb_view_holder_tag, null); 53 | viewHolder.view.setTag(R.id.lb_view_data_tag, null); 54 | ((ViewGroup) viewHolder.view).removeAllViews(); 55 | } 56 | }; 57 | } 58 | 59 | @Override 60 | public ViewHolder onCreateViewHolder(ViewGroup parent) { 61 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.hgv, parent, false); 62 | return new HorizontalItemViewHolder(view); 63 | } 64 | 65 | @Override 66 | public void onBindViewHolder(ViewHolder viewHolder, Object item) { 67 | HorizontalLayoutCollection collection = (HorizontalLayoutCollection) item; 68 | HorizontalItemViewHolder itemViewHolder = (HorizontalItemViewHolder) viewHolder; 69 | HorizontalGridView horizontalGridView = itemViewHolder.getHorizontalGridView(); 70 | ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(collection.getWidth(), collection.getHeight()); 71 | horizontalGridView.setLayoutParams(lp); 72 | ArrayObjectAdapter objectAdapter = new ArrayObjectAdapter(itemDockPresenter); 73 | ItemBridgeAdapter adapter = new ItemBridgeAdapter(objectAdapter); 74 | horizontalGridView.setAdapter(adapter); 75 | if (collection.getItems() != null) { 76 | objectAdapter.addAll(0, collection.getItems()); 77 | } 78 | } 79 | 80 | @Override 81 | public void onUnbindViewHolder(ViewHolder viewHolder) { 82 | } 83 | 84 | 85 | public final class HorizontalItemViewHolder extends ViewHolder { 86 | 87 | private HorizontalGridView horizontalGridView; 88 | 89 | public HorizontalItemViewHolder(View view) { 90 | super(view); 91 | horizontalGridView = (HorizontalGridView) view; 92 | } 93 | 94 | public HorizontalGridView getHorizontalGridView() { 95 | return horizontalGridView; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/view/FocusLineFeedFrameLayout.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout.view; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Rect; 6 | import androidx.annotation.Nullable; 7 | import android.util.AttributeSet; 8 | import android.util.Log; 9 | import android.view.KeyEvent; 10 | import android.view.ViewGroup; 11 | import android.widget.FrameLayout; 12 | 13 | import com.msisuzney.tv.waterfallayout.R; 14 | 15 | /** 16 | * @author: chenxin 17 | * @date: 2019-12-20 18 | * @email: chenxin7930@qq.com 19 | */ 20 | /** 21 | * 一种实现焦点换行的View,当View在屏幕右边缘时按下右键,焦点会换行到下一行的第一个View,左边缘同理换行到上一行最后一个View
    22 | *

    23 | * 使用 {@linkplain FocusLineFeedFrameLayout#setEdgeDistance(int)} 24 | * 设置距离屏幕边缘多少像素以内的View被认为是可以焦点换行的View
    25 | *

    26 | * 使用{@linkplain FocusLineFeedFrameLayout#setFocusLineFeed(boolean)}} 27 | * 设置是否开启焦点换行 28 | */ 29 | public class FocusLineFeedFrameLayout extends FrameLayout { 30 | 31 | private final static String TAG = "FLFFL"; 32 | private ViewGroup rootView; 33 | private Rect mRootViewRect = new Rect(); 34 | private Rect mTempDrawingRect = new Rect(); 35 | private int currentKeyCode = -1; 36 | private final int DEFAULT_DISTANCE = 120; 37 | /** 38 | * 距离屏幕边缘edgeDistance像素以内的子View被认为是换行View 39 | */ 40 | private int edgeDistance = DEFAULT_DISTANCE; 41 | 42 | /** 43 | * 是否开启焦点换行 44 | */ 45 | private boolean focusLineFeed = true; 46 | 47 | private boolean DEBUG = false; 48 | 49 | public FocusLineFeedFrameLayout(Context context) { 50 | super(context); 51 | } 52 | 53 | public FocusLineFeedFrameLayout(Context context, @Nullable AttributeSet attrs) { 54 | super(context, attrs); 55 | init(context, attrs); 56 | } 57 | 58 | public FocusLineFeedFrameLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 59 | super(context, attrs, defStyleAttr); 60 | init(context, attrs); 61 | } 62 | 63 | 64 | private void init(Context context, AttributeSet attrs) { 65 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FocusLineFeedFrameLayout); 66 | setFocusLineFeed(a.getBoolean(R.styleable.FocusLineFeedFrameLayout_focusLineFeed, true)); 67 | setEdgeDistance(a.getDimensionPixelSize(R.styleable.FocusLineFeedFrameLayout_edgeDistance, DEFAULT_DISTANCE)); 68 | a.recycle(); 69 | } 70 | 71 | public int getEdgeDistance() { 72 | return edgeDistance; 73 | } 74 | 75 | public void setEdgeDistance(int edgeDistance) { 76 | this.edgeDistance = edgeDistance; 77 | } 78 | 79 | public boolean isFocusLineFeed() { 80 | return focusLineFeed; 81 | } 82 | 83 | public void setFocusLineFeed(boolean focusLineFeed) { 84 | this.focusLineFeed = focusLineFeed; 85 | } 86 | 87 | @Override 88 | public final void getFocusedRect(Rect r) { 89 | if (focusLineFeed && isFocused()) { 90 | if (DEBUG) Log.d(TAG, "=========== focus start =============="); 91 | rootView.getDrawingRect(mRootViewRect); 92 | if (DEBUG) Log.d(TAG, "focus:true" + ",mRootViewRect:" + mRootViewRect); 93 | getDrawingRect(mTempDrawingRect); 94 | if (DEBUG) Log.d(TAG, "focus:true" + ",mTempDrawingRect:" + mTempDrawingRect); 95 | //转换子View到根布局的坐标系 96 | rootView.offsetDescendantRectToMyCoords(this, mTempDrawingRect); 97 | if (DEBUG) Log.d(TAG, "focus:true" + ",my location:" + mTempDrawingRect); 98 | //判断子View是否满足换行的条件 99 | if ((mTempDrawingRect.right + edgeDistance) > mRootViewRect.right && currentKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { 100 | getDrawingRect(mTempDrawingRect); 101 | rootView.offsetDescendantRectToMyCoords(this, mTempDrawingRect); 102 | if (DEBUG) Log.d(TAG, "focus:true" + ",my location2:" + mTempDrawingRect); 103 | if (DEBUG) 104 | Log.d(TAG, "focus:true" + ",measuredWidth:" + getMeasuredWidth() + ",measuredHeight:" + getMeasuredHeight()); 105 | mTempDrawingRect.left = 0 - getMeasuredWidth(); 106 | mTempDrawingRect.right = 0; 107 | mTempDrawingRect.bottom = mTempDrawingRect.bottom + getMeasuredHeight(); 108 | mTempDrawingRect.top = mTempDrawingRect.top + getMeasuredHeight(); 109 | if (DEBUG) Log.d(TAG, "focus:true" + ",focus rect location:" + mTempDrawingRect); 110 | //将focus rect的坐标系还原到子View的坐标系 111 | rootView.offsetRectIntoDescendantCoords(this, mTempDrawingRect); 112 | r.set(mTempDrawingRect); 113 | } else if (mTempDrawingRect.left - edgeDistance < mRootViewRect.left && currentKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) { 114 | getDrawingRect(mTempDrawingRect); 115 | rootView.offsetDescendantRectToMyCoords(this, mTempDrawingRect); 116 | mTempDrawingRect.left = mRootViewRect.right; 117 | mTempDrawingRect.right = mRootViewRect.right + getMeasuredWidth(); 118 | mTempDrawingRect.bottom = mTempDrawingRect.bottom - getMeasuredHeight(); 119 | mTempDrawingRect.top = mTempDrawingRect.top - getMeasuredHeight(); 120 | rootView.offsetRectIntoDescendantCoords(this, mTempDrawingRect); 121 | r.set(mTempDrawingRect); 122 | } else { 123 | super.getFocusedRect(r); 124 | } 125 | if (DEBUG) Log.d(TAG, "=========== focus end =============="); 126 | } else { 127 | super.getFocusedRect(r); 128 | } 129 | } 130 | 131 | @Override 132 | public boolean onKeyDown(int keyCode, KeyEvent event) { 133 | currentKeyCode = keyCode; 134 | return super.onKeyDown(keyCode, event); 135 | } 136 | 137 | @Override 138 | protected void onAttachedToWindow() { 139 | super.onAttachedToWindow(); 140 | rootView = (ViewGroup) getRootView(); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /demo/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/ArrayObjectAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.Collections; 19 | import java.util.List; 20 | 21 | /** 22 | * An {@link ObjectAdapter} implemented with an {@link ArrayList}. 23 | */ 24 | public class ArrayObjectAdapter extends ObjectAdapter { 25 | 26 | private ArrayList mItems = new ArrayList(); 27 | 28 | /** 29 | * Constructs an adapter with the given {@link PresenterSelector}. 30 | */ 31 | public ArrayObjectAdapter(PresenterSelector presenterSelector) { 32 | super(presenterSelector); 33 | } 34 | 35 | /** 36 | * Constructs an adapter that uses the given {@link Presenter} for all items. 37 | */ 38 | public ArrayObjectAdapter(Presenter presenter) { 39 | super(presenter); 40 | } 41 | 42 | /** 43 | * Constructs an adapter. 44 | */ 45 | public ArrayObjectAdapter() { 46 | super(); 47 | } 48 | 49 | @Override 50 | public int size() { 51 | return mItems.size(); 52 | } 53 | 54 | @Override 55 | public Object get(int index) { 56 | return mItems.get(index); 57 | } 58 | 59 | /** 60 | * Returns the index for the first occurrence of item in the adapter, or -1 if 61 | * not found. 62 | * 63 | * @param item The item to find in the list. 64 | * @return Index of the first occurrence of the item in the adapter, or -1 65 | * if not found. 66 | */ 67 | public int indexOf(Object item) { 68 | return mItems.indexOf(item); 69 | } 70 | 71 | /** 72 | * Notify that the content of a range of items changed. Note that this is 73 | * not same as items being added or removed. 74 | * 75 | * @param positionStart The position of first item that has changed. 76 | * @param itemCount The count of how many items have changed. 77 | */ 78 | public void notifyArrayItemRangeChanged(int positionStart, int itemCount) { 79 | notifyItemRangeChanged(positionStart, itemCount); 80 | } 81 | 82 | /** 83 | * Adds an item to the end of the adapter. 84 | * 85 | * @param item The item to add to the end of the adapter. 86 | */ 87 | public void add(Object item) { 88 | add(mItems.size(), item); 89 | } 90 | 91 | /** 92 | * Inserts an item into this adapter at the specified index. 93 | * If the index is >= {@link #size} an exception will be thrown. 94 | * 95 | * @param index The index at which the item should be inserted. 96 | * @param item The item to insert into the adapter. 97 | */ 98 | public void add(int index, Object item) { 99 | mItems.add(index, item); 100 | notifyItemRangeInserted(index, 1); 101 | } 102 | 103 | /** 104 | * Adds the objects in the given collection to the adapter, starting at the 105 | * given index. If the index is >= {@link #size} an exception will be thrown. 106 | * 107 | * @param index The index at which the items should be inserted. 108 | * @param items A {@link Collection} of items to insert. 109 | */ 110 | public void addAll(int index, Collection items) { 111 | int itemsCount = items.size(); 112 | if (itemsCount == 0) { 113 | return; 114 | } 115 | mItems.addAll(index, items); 116 | notifyItemRangeInserted(index, itemsCount); 117 | } 118 | 119 | /** 120 | * Removes the first occurrence of the given item from the adapter. 121 | * 122 | * @param item The item to remove from the adapter. 123 | * @return True if the item was found and thus removed from the adapter. 124 | */ 125 | public boolean remove(Object item) { 126 | int index = mItems.indexOf(item); 127 | if (index >= 0) { 128 | mItems.remove(index); 129 | notifyItemRangeRemoved(index, 1); 130 | } 131 | return index >= 0; 132 | } 133 | 134 | /** 135 | * Replaces item at position with a new item and calls notifyItemRangeChanged() 136 | * at the given position. Note that this method does not compare new item to 137 | * existing item. 138 | * @param position The index of item to replace. 139 | * @param item The new item to be placed at given position. 140 | */ 141 | public void replace(int position, Object item) { 142 | mItems.set(position, item); 143 | notifyItemRangeChanged(position, 1); 144 | } 145 | 146 | /** 147 | * Removes a range of items from the adapter. The range is specified by giving 148 | * the starting position and the number of elements to remove. 149 | * 150 | * @param position The index of the first item to remove. 151 | * @param count The number of items to remove. 152 | * @return The number of items removed. 153 | */ 154 | public int removeItems(int position, int count) { 155 | int itemsToRemove = Math.min(count, mItems.size() - position); 156 | if (itemsToRemove <= 0) { 157 | return 0; 158 | } 159 | 160 | for (int i = 0; i < itemsToRemove; i++) { 161 | mItems.remove(position); 162 | } 163 | notifyItemRangeRemoved(position, itemsToRemove); 164 | return itemsToRemove; 165 | } 166 | 167 | /** 168 | * Removes all items from this adapter, leaving it empty. 169 | */ 170 | public void clear() { 171 | int itemCount = mItems.size(); 172 | if (itemCount == 0) { 173 | return; 174 | } 175 | mItems.clear(); 176 | notifyItemRangeRemoved(0, itemCount); 177 | } 178 | 179 | /** 180 | * Gets a read-only view of the list of object of this ArrayObjectAdapter. 181 | */ 182 | public List unmodifiableList() { 183 | return Collections.unmodifiableList((List) mItems); 184 | } 185 | 186 | @Override 187 | public boolean isImmediateNotifySupported() { 188 | return true; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/ItemAlignmentFacet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import androidx.recyclerview.widget.RecyclerView; 17 | import android.view.View; 18 | 19 | /** 20 | * Optional facet provided by {@link RecyclerView.Adapter} or {@link RecyclerView.ViewHolder} for 21 | * use in {@link HorizontalGridView} and {@link VerticalGridView}. Apps using {@link Presenter} may 22 | * set facet using {@link Presenter#setFacet(Class, Object)} or 23 | * {@link Presenter.ViewHolder#setFacet(Class, Object)}. Facet on ViewHolder has a higher priority 24 | * than Presenter or Adapter. 25 | *

    26 | * ItemAlignmentFacet contains single or multiple {@link ItemAlignmentDef}s. First 27 | * {@link ItemAlignmentDef} describes the default alignment position for ViewHolder, it also 28 | * overrides the default item alignment settings on {@link VerticalGridView} and 29 | * {@link HorizontalGridView}. When there are multiple {@link ItemAlignmentDef}s, the extra 30 | * {@link ItemAlignmentDef}s are used to calculate deltas from first alignment position. When a 31 | * descendant view is focused within the ViewHolder, grid view will visit focused view and its 32 | * ancestors till the root of ViewHolder to match extra {@link ItemAlignmentDef}s' 33 | * {@link ItemAlignmentDef#getItemAlignmentViewId()}. Once a match found, the 34 | * {@link ItemAlignmentDef} is used to adjust a scroll delta from default alignment position. 35 | */ 36 | public final class ItemAlignmentFacet { 37 | 38 | /** 39 | * Value indicates that percent is not used. 40 | */ 41 | public final static float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1; 42 | 43 | /** 44 | * Definition of an alignment position under a view. 45 | */ 46 | public static class ItemAlignmentDef { 47 | int mViewId = View.NO_ID; 48 | int mFocusViewId = View.NO_ID; 49 | int mOffset = 0; 50 | float mOffsetPercent = 50f; 51 | boolean mOffsetWithPadding = false; 52 | private boolean mAlignToBaseline; 53 | 54 | /** 55 | * Sets number of pixels to offset. Can be negative for alignment from the high edge, or 56 | * positive for alignment from the low edge. 57 | */ 58 | public final void setItemAlignmentOffset(int offset) { 59 | mOffset = offset; 60 | } 61 | 62 | /** 63 | * Gets number of pixels to offset. Can be negative for alignment from the high edge, or 64 | * positive for alignment from the low edge. 65 | */ 66 | public final int getItemAlignmentOffset() { 67 | return mOffset; 68 | } 69 | 70 | /** 71 | * Sets whether to include left/top padding for positive item offset, include 72 | * right/bottom padding for negative item offset. 73 | */ 74 | public final void setItemAlignmentOffsetWithPadding(boolean withPadding) { 75 | mOffsetWithPadding = withPadding; 76 | } 77 | 78 | /** 79 | * When it is true: we include left/top padding for positive item offset, include 80 | * right/bottom padding for negative item offset. 81 | */ 82 | public final boolean isItemAlignmentOffsetWithPadding() { 83 | return mOffsetWithPadding; 84 | } 85 | 86 | /** 87 | * Sets the offset percent for item alignment in addition to offset. E.g., 40 88 | * means 40% of the width from the low edge. Use {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} 89 | * to disable. 90 | */ 91 | public final void setItemAlignmentOffsetPercent(float percent) { 92 | if ((percent < 0 || percent > 100) 93 | && percent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) { 94 | throw new IllegalArgumentException(); 95 | } 96 | mOffsetPercent = percent; 97 | } 98 | 99 | /** 100 | * Gets the offset percent for item alignment in addition to offset. E.g., 40 101 | * means 40% of the width from the low edge. Use {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} 102 | * to disable. 103 | */ 104 | public final float getItemAlignmentOffsetPercent() { 105 | return mOffsetPercent; 106 | } 107 | 108 | /** 109 | * Sets Id of which child view to be aligned. View.NO_ID refers to root view and should 110 | * be only used in first one. Extra ItemAlignmentDefs should provide view id to match 111 | * currently focused view. 112 | */ 113 | public final void setItemAlignmentViewId(int viewId) { 114 | mViewId = viewId; 115 | } 116 | 117 | /** 118 | * Gets Id of which child view to be aligned. View.NO_ID refers to root view and should 119 | * be only used in first one. Extra ItemAlignmentDefs should provide view id to match 120 | * currently focused view. 121 | */ 122 | public final int getItemAlignmentViewId() { 123 | return mViewId; 124 | } 125 | 126 | /** 127 | * Sets Id of which child view take focus for alignment. When not set, it will use 128 | * use same id of {@link #getItemAlignmentViewId()} 129 | */ 130 | public final void setItemAlignmentFocusViewId(int viewId) { 131 | mFocusViewId = viewId; 132 | } 133 | 134 | /** 135 | * Returns Id of which child view take focus for alignment. When not set, it will use 136 | * use same id of {@link #getItemAlignmentViewId()} 137 | */ 138 | public final int getItemAlignmentFocusViewId() { 139 | return mFocusViewId != View.NO_ID ? mFocusViewId : mViewId; 140 | } 141 | 142 | /** 143 | * Align to baseline if {@link #getItemAlignmentViewId()} is a TextView and 144 | * alignToBaseline is true. 145 | * @param alignToBaseline Boolean indicating whether to align the text to baseline. 146 | */ 147 | public final void setAlignedToTextViewBaseline(boolean alignToBaseline) { 148 | this.mAlignToBaseline = alignToBaseline; 149 | } 150 | 151 | /** 152 | * Returns true when TextView should be aligned to the baseline. 153 | */ 154 | public boolean isAlignedToTextViewBaseLine() { 155 | return mAlignToBaseline; 156 | } 157 | } 158 | 159 | private ItemAlignmentDef[] mAlignmentDefs = new ItemAlignmentDef[]{new ItemAlignmentDef()}; 160 | 161 | public boolean isMultiAlignment() { 162 | return mAlignmentDefs.length > 1; 163 | } 164 | 165 | /** 166 | * Sets definitions of alignment positions. 167 | */ 168 | public void setAlignmentDefs(ItemAlignmentDef[] defs) { 169 | if (defs == null || defs.length < 1) { 170 | throw new IllegalArgumentException(); 171 | } 172 | mAlignmentDefs = defs; 173 | } 174 | 175 | /** 176 | * Returns read only definitions of alignment positions. 177 | */ 178 | public ItemAlignmentDef[] getAlignmentDefs() { 179 | return mAlignmentDefs; 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/SingleRow.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import androidx.annotation.NonNull; 17 | import androidx.collection.CircularIntArray; 18 | import androidx.recyclerview.widget.RecyclerView; 19 | 20 | import java.io.PrintWriter; 21 | 22 | /** 23 | * A Grid with restriction to single row. 24 | */ 25 | class SingleRow extends Grid { 26 | 27 | private final Location mTmpLocation = new Location(0); 28 | private Object[] mTmpItem = new Object[1]; 29 | 30 | SingleRow() { 31 | setNumRows(1); 32 | } 33 | 34 | @Override 35 | public final Location getLocation(int index) { 36 | // all items are on row 0, share the same Location object. 37 | return mTmpLocation; 38 | } 39 | 40 | @Override 41 | public final void debugPrint(PrintWriter pw) { 42 | pw.print("SingleRow<"); 43 | pw.print(mFirstVisibleIndex); 44 | pw.print(","); 45 | pw.print(mLastVisibleIndex); 46 | pw.print(">"); 47 | pw.println(); 48 | } 49 | 50 | int getStartIndexForAppend() { 51 | if (mLastVisibleIndex >= 0) { 52 | return mLastVisibleIndex + 1; 53 | } else if (mStartIndex != START_DEFAULT) { 54 | return Math.min(mStartIndex, mProvider.getCount() - 1); 55 | } else { 56 | return 0; 57 | } 58 | } 59 | 60 | int getStartIndexForPrepend() { 61 | if (mFirstVisibleIndex >= 0) { 62 | return mFirstVisibleIndex - 1; 63 | } else if (mStartIndex != START_DEFAULT) { 64 | return Math.min(mStartIndex, mProvider.getCount() - 1); 65 | } else { 66 | return mProvider.getCount() - 1; 67 | } 68 | } 69 | 70 | @Override 71 | protected final boolean prependVisibleItems(int toLimit, boolean oneColumnMode) { 72 | if (mProvider.getCount() == 0) { 73 | return false; 74 | } 75 | if (!oneColumnMode && checkPrependOverLimit(toLimit)) { 76 | return false; 77 | } 78 | boolean filledOne = false; 79 | for (int index = getStartIndexForPrepend(); index >= 0; index--) { 80 | int size = mProvider.createItem(index, false, mTmpItem); 81 | int edge; 82 | if (mFirstVisibleIndex < 0 || mLastVisibleIndex < 0) { 83 | edge = mReversedFlow ? Integer.MIN_VALUE : Integer.MAX_VALUE; 84 | mLastVisibleIndex = mFirstVisibleIndex = index; 85 | } else { 86 | if (mReversedFlow) { 87 | edge = mProvider.getEdge(index + 1) + mSpacing + size; 88 | } else { 89 | edge = mProvider.getEdge(index + 1) - mSpacing - size; 90 | } 91 | mFirstVisibleIndex = index; 92 | } 93 | mProvider.addItem(mTmpItem[0], index, size, 0, edge); 94 | filledOne = true; 95 | if (oneColumnMode || checkPrependOverLimit(toLimit)) { 96 | break; 97 | } 98 | } 99 | return filledOne; 100 | } 101 | 102 | @Override 103 | protected final boolean appendVisibleItems(int toLimit, boolean oneColumnMode) { 104 | if (mProvider.getCount() == 0) { 105 | return false; 106 | } 107 | if (!oneColumnMode && checkAppendOverLimit(toLimit)) { 108 | // not in one column mode, return immediately if over limit 109 | return false; 110 | } 111 | boolean filledOne = false; 112 | for (int index = getStartIndexForAppend(); index < mProvider.getCount(); index++) { 113 | int size = mProvider.createItem(index, true, mTmpItem); 114 | int edge; 115 | if (mFirstVisibleIndex < 0 || mLastVisibleIndex< 0) { 116 | edge = mReversedFlow ? Integer.MAX_VALUE : Integer.MIN_VALUE; 117 | mLastVisibleIndex = mFirstVisibleIndex = index; 118 | } else { 119 | if (mReversedFlow) { 120 | edge = mProvider.getEdge(index - 1) - mProvider.getSize(index - 1) - mSpacing; 121 | } else { 122 | edge = mProvider.getEdge(index - 1) + mProvider.getSize(index - 1) + mSpacing; 123 | } 124 | mLastVisibleIndex = index; 125 | } 126 | mProvider.addItem(mTmpItem[0], index, size, 0, edge); 127 | filledOne = true; 128 | if (oneColumnMode || checkAppendOverLimit(toLimit)) { 129 | break; 130 | } 131 | } 132 | return filledOne; 133 | } 134 | 135 | @Override 136 | public void collectAdjacentPrefetchPositions(int fromLimit, int da, 137 | @NonNull RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry) { 138 | int indexToPrefetch; 139 | int nearestEdge; 140 | if (mReversedFlow ? da > 0 : da < 0) { 141 | // prefetch next prepend, lower index number 142 | if (getFirstVisibleIndex() == 0) { 143 | return; // no remaining items to prefetch 144 | } 145 | 146 | indexToPrefetch = getStartIndexForPrepend(); 147 | nearestEdge = mProvider.getEdge(mFirstVisibleIndex) 148 | + (mReversedFlow ? mSpacing : -mSpacing); 149 | } else { 150 | // prefetch next append, higher index number 151 | if (getLastVisibleIndex() == mProvider.getCount() - 1) { 152 | return; // no remaining items to prefetch 153 | } 154 | 155 | indexToPrefetch = getStartIndexForAppend(); 156 | int itemSizeWithSpace = mProvider.getSize(mLastVisibleIndex) + mSpacing; 157 | nearestEdge = mProvider.getEdge(mLastVisibleIndex) 158 | + (mReversedFlow ? -itemSizeWithSpace : itemSizeWithSpace); 159 | } 160 | 161 | int distance = Math.abs(nearestEdge - fromLimit); 162 | layoutPrefetchRegistry.addPosition(indexToPrefetch, distance); 163 | } 164 | 165 | @Override 166 | public final CircularIntArray[] getItemPositionsInRows(int startPos, int endPos) { 167 | // all items are on the same row: 168 | mTmpItemPositionsInRows[0].clear(); 169 | mTmpItemPositionsInRows[0].addLast(startPos); 170 | mTmpItemPositionsInRows[0].addLast(endPos); 171 | return mTmpItemPositionsInRows; 172 | } 173 | 174 | @Override 175 | protected final int findRowMin(boolean findLarge, int indexLimit, int[] indices) { 176 | if (indices != null) { 177 | indices[0] = 0; 178 | indices[1] = indexLimit; 179 | } 180 | return mReversedFlow ? mProvider.getEdge(indexLimit) - mProvider.getSize(indexLimit) 181 | : mProvider.getEdge(indexLimit); 182 | } 183 | 184 | @Override 185 | protected final int findRowMax(boolean findLarge, int indexLimit, int[] indices) { 186 | if (indices != null) { 187 | indices[0] = 0; 188 | indices[1] = indexLimit; 189 | } 190 | return mReversedFlow ? mProvider.getEdge(indexLimit) 191 | : mProvider.getEdge(indexLimit) + mProvider.getSize(indexLimit); 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/RowsFragment.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.waterfallayout; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | 6 | import androidx.annotation.Nullable; 7 | import androidx.fragment.app.Fragment; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | import android.util.Log; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | 15 | import com.msisuzney.tv.waterfallayout.R; 16 | import com.msisuzney.tv.waterfallayout.leanback.ArrayObjectAdapter; 17 | import com.msisuzney.tv.waterfallayout.leanback.HorizontalGridView; 18 | import com.msisuzney.tv.waterfallayout.leanback.HorizontalGridView; 19 | import com.msisuzney.tv.waterfallayout.leanback.ItemBridgeAdapter; 20 | import com.msisuzney.tv.waterfallayout.leanback.PresenterSelector; 21 | import com.msisuzney.tv.waterfallayout.leanback.VerticalGridView; 22 | import com.msisuzney.tv.waterfallayout.presenter.RowPresenterSelector; 23 | 24 | import java.util.Collection; 25 | 26 | /** 27 | * @author: chenxin 28 | * @date: 2019-12-20 29 | * @email: chenxin7930@qq.com 30 | */ 31 | 32 | /** 33 | * 以行(Row)作为最小运营单元的瀑布流竖直布局

    34 | * 行的定义:
    35 | * 每行的布局可以是 36 | *

      37 | *
    1. 绝对布局,{@linkplain android.widget.AbsoluteLayout}
    2. 38 | *
    3. 水平方向滑动布局,{@linkplain HorizontalGridView}
    4. 39 | *
    5. 自定义布局,需要重写{@linkplain RowsFragment#initOtherPresenterSelector()} 提供View 40 | *
    6. 41 | *
    42 | * 43 | *

    44 | * 其中,绝对布局和水平方向滑动布局必须使用{@linkplain com.msisuzney.tv.waterfallayout.model.Collection} 提供宽高, 45 | * 以及必须使用{@linkplain com.msisuzney.tv.waterfallayout.model.Item} 提供布局中每个View的宽高和数据

    46 | * 行中View的定义:
    47 | * 绝对布局和水平方向滑动布局中的View 48 | * 需要重写 {@linkplain RowsFragment#initBlockPresenterSelector} 提供行中的运营位布局的选择器 49 | *
    50 | *
    51 | */ 52 | public abstract class RowsFragment extends Fragment { 53 | 54 | public static final String TAG = "RowsFragment"; 55 | private VerticalGridView mVerticalGridView; 56 | private StateChangeObservable stateChangeObservable; 57 | private int leftPadding; 58 | private int topPadding; 59 | private int rightPadding; 60 | private int bottomPadding; 61 | private ArrayObjectAdapter mAdapter; 62 | 63 | @Override 64 | public void onAttach(Context context) { 65 | super.onAttach(context); 66 | stateChangeObservable = initStateChangeObservable(); 67 | } 68 | 69 | @Nullable 70 | @Override 71 | public final View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 72 | return inflater.inflate(R.layout.fragment_waterfall, container, false); 73 | } 74 | 75 | @Override 76 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 77 | super.onViewCreated(view, savedInstanceState); 78 | mAdapter = initAdapter(); 79 | mVerticalGridView = view.findViewById(R.id.vgv); 80 | mVerticalGridView.setPadding(leftPadding, topPadding, rightPadding, bottomPadding); 81 | if (mOnRowSelectedListener != null) { 82 | mVerticalGridView.setOnChildViewHolderSelectedListener(mOnRowSelectedListener); 83 | } 84 | mVerticalGridView.addOnScrollListener(new RecyclerView.OnScrollListener() { 85 | @Override 86 | public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 87 | stateChangeObservable.onScrollStateChanged(recyclerView, newState); 88 | } 89 | }); 90 | mVerticalGridView.setItemAnimator(null); 91 | 92 | ItemBridgeAdapter itemBridgeAdapter = new ItemBridgeAdapter(mAdapter); 93 | mVerticalGridView.setAdapter(itemBridgeAdapter); 94 | } 95 | 96 | protected StateChangeObservable initStateChangeObservable() { 97 | return new StateChangeObservable() { 98 | }; 99 | } 100 | 101 | 102 | @Override 103 | public void onResume() { 104 | super.onResume(); 105 | if (isVisible) {//第一个Fragment加载时或者现加载Fragment时setUserVisibleHint在onAttach之前调用的,保存可见状态到onResume时通知 106 | Log.d(TAG, "visible@" + hashCode() + "," + isVisible); 107 | stateChangeObservable.notifyFragmentVisibilityChanged(isVisible); 108 | } 109 | } 110 | 111 | /** 112 | * Fragment是否可见 113 | */ 114 | private boolean isVisible; 115 | 116 | //onAttach之前或者Fragment已经创建好时可见调用 117 | @Override 118 | public void setUserVisibleHint(boolean isVisibleToUser) { 119 | super.setUserVisibleHint(isVisibleToUser); 120 | this.isVisible = isVisibleToUser; 121 | if (isResumed()) {//onCreateView执行后mSelector!= null,才开始监听 122 | Log.d(TAG, "visible@" + hashCode() + "," + isVisibleToUser); 123 | stateChangeObservable.notifyFragmentVisibilityChanged(isVisible); 124 | } 125 | } 126 | 127 | 128 | @Override 129 | public void onPause() { 130 | super.onPause(); 131 | stateChangeObservable.notifyFragmentPause(); 132 | } 133 | 134 | /** 135 | * 设置RecyclerView的padding 136 | * 137 | * @param left 138 | * @param top 139 | * @param right 140 | * @param bottom 141 | */ 142 | protected void setPadding(int left, int top, int right, int bottom) { 143 | this.leftPadding = left; 144 | this.topPadding = top; 145 | this.rightPadding = right; 146 | this.bottomPadding = bottom; 147 | } 148 | 149 | 150 | /** 151 | * 有多个View更新或者获取时,使用runnable封装,保证操作按顺序进行 152 | * 153 | * @param runnable 154 | */ 155 | protected void postRefreshRunnable(Runnable runnable) { 156 | mVerticalGridView.post(runnable); 157 | } 158 | 159 | 160 | private OnRowSelectedListener mOnRowSelectedListener; 161 | 162 | /** 163 | * Sets an item selection listener. 164 | */ 165 | public void setOnRowSelectedListener(OnRowSelectedListener listener) { 166 | mOnRowSelectedListener = listener; 167 | } 168 | 169 | 170 | private ArrayObjectAdapter initAdapter() { 171 | PresenterSelector blockSelector = initBlockPresenterSelector(); 172 | if (blockSelector == null) { 173 | throw new RuntimeException("BlockPresenterSelector must not be null"); 174 | } 175 | PresenterSelector otherPresenterSelector = initOtherPresenterSelector(); 176 | RowPresenterSelector mSelector = new RowPresenterSelector(blockSelector); 177 | mSelector.setOtherPresenterSelector(otherPresenterSelector); 178 | 179 | return new ArrayObjectAdapter(mSelector); 180 | } 181 | 182 | 183 | /** 184 | * 行中的运营位的选择器,不能为null 185 | * 186 | * @return 187 | */ 188 | protected abstract PresenterSelector initBlockPresenterSelector(); 189 | 190 | /** 191 | * 与行同级但不是行的其他布局的选择器 192 | * 193 | * @return 194 | */ 195 | protected PresenterSelector initOtherPresenterSelector() { 196 | return null; 197 | } 198 | 199 | 200 | @Override 201 | public void onDestroyView() { 202 | super.onDestroyView(); 203 | stateChangeObservable.unregisterAll(); 204 | } 205 | 206 | //===ArrayObjectAdapter中的方法=== 207 | 208 | protected void addAll(int index, Collection objects) { 209 | mAdapter.addAll(index, objects); 210 | } 211 | 212 | protected void remove(Object item) { 213 | mAdapter.remove(item); 214 | } 215 | 216 | protected int size() { 217 | return mAdapter.size(); 218 | } 219 | 220 | protected void add(Object item) { 221 | mAdapter.add(item); 222 | } 223 | 224 | protected void replace(int position, Object item) { 225 | mAdapter.replace(position, item); 226 | } 227 | 228 | protected void add(int index, Object item) { 229 | mAdapter.add(index, item); 230 | } 231 | 232 | protected int indexOf(Object item) { 233 | return mAdapter.indexOf(item); 234 | } 235 | 236 | protected void notifyArrayItemRangeChanged(int positionStart, int itemCount) { 237 | mAdapter.notifyArrayItemRangeChanged(positionStart, itemCount); 238 | } 239 | 240 | protected void clear() { 241 | mAdapter.clear(); 242 | } 243 | 244 | protected void addOnScrollListener(RecyclerView.OnScrollListener scrollChangeListener) { 245 | mVerticalGridView.addOnScrollListener(scrollChangeListener); 246 | } 247 | 248 | protected void scrollToPosition(int position) { 249 | mVerticalGridView.smoothScrollToPosition(position); 250 | } 251 | 252 | protected int removeItems(int position, int count) { 253 | return mAdapter.removeItems(position, count); 254 | } 255 | 256 | } 257 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/ViewsStateBundle.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import android.os.Bundle; 17 | import android.os.Parcelable; 18 | import androidx.collection.LruCache; 19 | import android.util.SparseArray; 20 | import android.view.View; 21 | 22 | import java.util.Iterator; 23 | import java.util.Map; 24 | import java.util.Map.Entry; 25 | 26 | import static com.msisuzney.tv.waterfallayout.leanback.BaseGridView.SAVE_NO_CHILD; 27 | import static com.msisuzney.tv.waterfallayout.leanback.BaseGridView.SAVE_ON_SCREEN_CHILD; 28 | import static com.msisuzney.tv.waterfallayout.leanback.BaseGridView.SAVE_LIMITED_CHILD; 29 | import static com.msisuzney.tv.waterfallayout.leanback.BaseGridView.SAVE_ALL_CHILD; 30 | 31 | /** 32 | * Maintains a bundle of states for a group of views. Each view must have a unique id to identify 33 | * it. There are four different strategies {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD} 34 | * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}. 35 | *

    36 | * This class serves purpose of nested "listview" e.g. a vertical list of horizontal list. 37 | * Vertical list maintains id->bundle mapping of all its children (even the children is offscreen 38 | * and being pruned). 39 | *

    40 | * The class is currently used within {@link GridLayoutManager}, but it might be used by other 41 | * ViewGroup. 42 | */ 43 | class ViewsStateBundle { 44 | 45 | public static final int LIMIT_DEFAULT = 100; 46 | public static final int UNLIMITED = Integer.MAX_VALUE; 47 | 48 | private int mSavePolicy; 49 | private int mLimitNumber; 50 | 51 | private LruCache> mChildStates; 52 | 53 | public ViewsStateBundle() { 54 | mSavePolicy = SAVE_NO_CHILD; 55 | mLimitNumber = LIMIT_DEFAULT; 56 | } 57 | 58 | public void clear() { 59 | if (mChildStates != null) { 60 | mChildStates.evictAll(); 61 | } 62 | } 63 | 64 | public void remove(int id) { 65 | if (mChildStates != null && mChildStates.size() != 0) { 66 | mChildStates.remove(getSaveStatesKey(id)); 67 | } 68 | } 69 | 70 | /** 71 | * @return the saved views states 72 | */ 73 | public final Bundle saveAsBundle() { 74 | if (mChildStates == null || mChildStates.size() == 0) { 75 | return null; 76 | } 77 | Map> snapshot = mChildStates.snapshot(); 78 | Bundle bundle = new Bundle(); 79 | for (Iterator>> i = 80 | snapshot.entrySet().iterator(); i.hasNext(); ) { 81 | Entry> e = i.next(); 82 | bundle.putSparseParcelableArray(e.getKey(), e.getValue()); 83 | } 84 | return bundle; 85 | } 86 | 87 | public final void loadFromBundle(Bundle savedBundle) { 88 | if (mChildStates != null && savedBundle != null) { 89 | mChildStates.evictAll(); 90 | for (Iterator i = savedBundle.keySet().iterator(); i.hasNext(); ) { 91 | String key = i.next(); 92 | mChildStates.put(key, savedBundle.getSparseParcelableArray(key)); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * @return the savePolicy, see {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD} 99 | * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD} 100 | */ 101 | public final int getSavePolicy() { 102 | return mSavePolicy; 103 | } 104 | 105 | /** 106 | * @return the limitNumber, only works when {@link #getSavePolicy()} is 107 | * {@link #SAVE_LIMITED_CHILD} 108 | */ 109 | public final int getLimitNumber() { 110 | return mLimitNumber; 111 | } 112 | 113 | /** 114 | * @see ViewsStateBundle#getSavePolicy() 115 | */ 116 | public final void setSavePolicy(int savePolicy) { 117 | this.mSavePolicy = savePolicy; 118 | applyPolicyChanges(); 119 | } 120 | 121 | /** 122 | * @see ViewsStateBundle#getLimitNumber() 123 | */ 124 | public final void setLimitNumber(int limitNumber) { 125 | this.mLimitNumber = limitNumber; 126 | applyPolicyChanges(); 127 | } 128 | 129 | protected void applyPolicyChanges() { 130 | if (mSavePolicy == SAVE_LIMITED_CHILD) { 131 | if (mLimitNumber <= 0) { 132 | throw new IllegalArgumentException(); 133 | } 134 | if (mChildStates == null || mChildStates.maxSize() != mLimitNumber) { 135 | mChildStates = new LruCache>(mLimitNumber); 136 | } 137 | } else if (mSavePolicy == SAVE_ALL_CHILD || mSavePolicy == SAVE_ON_SCREEN_CHILD) { 138 | if (mChildStates == null || mChildStates.maxSize() != UNLIMITED) { 139 | mChildStates = new LruCache>(UNLIMITED); 140 | } 141 | } else { 142 | mChildStates = null; 143 | } 144 | } 145 | 146 | /** 147 | * Load view from states, it's none operation if the there is no state associated with the id. 148 | * 149 | * @param view view where loads into 150 | * @param id unique id for the view within this ViewsStateBundle 151 | */ 152 | public final void loadView(View view, int id) { 153 | if (mChildStates != null) { 154 | String key = getSaveStatesKey(id); 155 | // Once loaded the state, do not keep the state of child. The child state will 156 | // be saved again either when child is offscreen or when the parent is saved. 157 | SparseArray container = mChildStates.remove(key); 158 | if (container != null) { 159 | view.restoreHierarchyState(container); 160 | } 161 | } 162 | } 163 | 164 | /** 165 | * Save views regardless what's the current policy is. 166 | * 167 | * @param view view to save 168 | * @param id unique id for the view within this ViewsStateBundle 169 | */ 170 | protected final void saveViewUnchecked(View view, int id) { 171 | if (mChildStates != null) { 172 | String key = getSaveStatesKey(id); 173 | SparseArray container = new SparseArray(); 174 | view.saveHierarchyState(container); 175 | mChildStates.put(key, container); 176 | } 177 | } 178 | 179 | /** 180 | * The on screen view is saved when policy is not {@link #SAVE_NO_CHILD}. 181 | * 182 | * @param bundle Bundle where we save the on screen view state. If null, 183 | * a new Bundle is created and returned. 184 | * @param view The view to save. 185 | * @param id Id of the view. 186 | */ 187 | public final Bundle saveOnScreenView(Bundle bundle, View view, int id) { 188 | if (mSavePolicy != SAVE_NO_CHILD) { 189 | String key = getSaveStatesKey(id); 190 | SparseArray container = new SparseArray(); 191 | view.saveHierarchyState(container); 192 | if (bundle == null) { 193 | bundle = new Bundle(); 194 | } 195 | bundle.putSparseParcelableArray(key, container); 196 | } 197 | return bundle; 198 | } 199 | 200 | /** 201 | * Save off screen views according to policy. 202 | * 203 | * @param view view to save 204 | * @param id unique id for the view within this ViewsStateBundle 205 | */ 206 | public final void saveOffscreenView(View view, int id) { 207 | switch (mSavePolicy) { 208 | case SAVE_LIMITED_CHILD: 209 | case SAVE_ALL_CHILD: 210 | saveViewUnchecked(view, id); 211 | break; 212 | case SAVE_ON_SCREEN_CHILD: 213 | remove(id); 214 | break; 215 | default: 216 | break; 217 | } 218 | } 219 | 220 | static String getSaveStatesKey(int id) { 221 | return Integer.toString(id); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/Presenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | 19 | import androidx.recyclerview.widget.RecyclerView; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | /** 25 | * A Presenter is used to generate {@link View}s and bind Objects to them on 26 | * demand. It is closely related to the concept of an {@link 27 | * RecyclerView.Adapter RecyclerView.Adapter}, but is 28 | * not position-based. The leanback framework implements the adapter concept using 29 | * {@link ObjectAdapter} which refers to a Presenter (or {@link PresenterSelector}) instance. 30 | * 31 | *

    32 | * Presenters should be stateless. Presenters typically extend {@link ViewHolder} to store all 33 | * necessary view state information, such as references to child views to be used when 34 | * binding to avoid expensive calls to {@link View#findViewById(int)}. 35 | *

    36 | * 37 | *

    38 | * A trivial Presenter that takes a string and renders it into a {@link 39 | * android.widget.TextView TextView}: 40 | * 41 | *

     42 |  * public class StringTextViewPresenter extends Presenter {
     43 |  *     // This class does not need a custom ViewHolder, since it does not use
     44 |  *     // a complex layout.
     45 |  *
     46 |  *     {@literal @}Override
     47 |  *     public ViewHolder onCreateViewHolder(ViewGroup parent) {
     48 |  *         return new ViewHolder(new TextView(parent.getContext()));
     49 |  *     }
     50 |  *
     51 |  *     {@literal @}Override
     52 |  *     public void onBindViewHolder(ViewHolder viewHolder, Object item) {
     53 |  *         String str = (String) item;
     54 |  *         TextView textView = (TextView) viewHolder.mView;
     55 |  *
     56 |  *         textView.setText(item);
     57 |  *     }
     58 |  *
     59 |  *     {@literal @}Override
     60 |  *     public void onUnbindViewHolder(ViewHolder viewHolder) {
     61 |  *         // Nothing to unbind for TextView, but if this viewHolder had
     62 |  *         // allocated bitmaps, they can be released here.
     63 |  *     }
     64 |  * }
     65 |  * 
    66 | * In addition to view creation and binding, Presenter allows dynamic interface (facet) to 67 | * be added: {@link #setFacet(Class, Object)}. Supported facets: 68 | *
  • {@link ItemAlignmentFacet} is used by {@link HorizontalGridView} and 69 | * {@link VerticalGridView} to customize child alignment. 70 | */ 71 | public abstract class Presenter implements FacetProvider { 72 | /** 73 | * ViewHolder can be subclassed and used to cache any view accessors needed 74 | * to improve binding performance (for example, results of findViewById) 75 | * without needing to subclass a View. 76 | */ 77 | public static class ViewHolder implements FacetProvider { 78 | public final View view; 79 | private Map mFacets; 80 | 81 | public ViewHolder(View view) { 82 | this.view = view; 83 | } 84 | 85 | @Override 86 | public final Object getFacet(Class facetClass) { 87 | if (mFacets == null) { 88 | return null; 89 | } 90 | return mFacets.get(facetClass); 91 | } 92 | 93 | /** 94 | * Sets dynamic implemented facet in addition to basic ViewHolder functions. 95 | * 96 | * @param facetClass Facet classes to query, can be class of {@link ItemAlignmentFacet}. 97 | * @param facetImpl Facet implementation. 98 | */ 99 | public final void setFacet(Class facetClass, Object facetImpl) { 100 | if (mFacets == null) { 101 | mFacets = new HashMap(); 102 | } 103 | mFacets.put(facetClass, facetImpl); 104 | } 105 | } 106 | 107 | /** 108 | * Base class to perform a task on Presenter.ViewHolder. 109 | */ 110 | public static abstract class ViewHolderTask { 111 | /** 112 | * Called to perform a task on view holder. 113 | * 114 | * @param holder The view holder to perform task. 115 | */ 116 | public void run(ViewHolder holder) { 117 | } 118 | } 119 | 120 | private Map mFacets; 121 | 122 | /** 123 | * Creates a new {@link View}. 124 | */ 125 | public abstract ViewHolder onCreateViewHolder(ViewGroup parent); 126 | 127 | /** 128 | * Binds a {@link View} to an item. 129 | */ 130 | public abstract void onBindViewHolder(ViewHolder viewHolder, Object item); 131 | 132 | /** 133 | * Unbinds a {@link View} from an item. Any expensive references may be 134 | * released here, and any fields that are not bound for every item should be 135 | * cleared here. 136 | */ 137 | public abstract void onUnbindViewHolder(ViewHolder viewHolder); 138 | 139 | /** 140 | * Called when a view created by this presenter has been attached to a window. 141 | * 142 | *

    This can be used as a reasonable signal that the view is about to be seen 143 | * by the user. If the adapter previously freed any resources in 144 | * {@link #onViewDetachedFromWindow(ViewHolder)} 145 | * those resources should be restored here.

    146 | * 147 | * @param holder Holder of the view being attached 148 | */ 149 | public void onViewAttachedToWindow(ViewHolder holder) { 150 | } 151 | 152 | /** 153 | * Called when a view created by this presenter has been detached from its window. 154 | * 155 | *

    Becoming detached from the window is not necessarily a permanent condition; 156 | * the consumer of an presenter's views may choose to cache views offscreen while they 157 | * are not visible, attaching and detaching them as appropriate.

    158 | *

    159 | * Any view property animations should be cancelled here or the view may fail 160 | * to be recycled. 161 | * 162 | * @param holder Holder of the view being detached 163 | */ 164 | public void onViewDetachedFromWindow(ViewHolder holder) { 165 | // If there are view property animations running then RecyclerView won't recycle. 166 | // cancelAnimationsRecursive(holder.view); 167 | } 168 | //兼容API 14 169 | // /** 170 | // * Utility method for removing all running animations on a view. 171 | // */ 172 | // protected static void cancelAnimationsRecursive(View view) { 173 | // if (view != null && view.hasTransientState()) { 174 | // view.animate().cancel(); 175 | // if (view instanceof ViewGroup) { 176 | // final int count = ((ViewGroup) view).getChildCount(); 177 | // for (int i = 0; view.hasTransientState() && i < count; i++) { 178 | // cancelAnimationsRecursive(((ViewGroup) view).getChildAt(i)); 179 | // } 180 | // } 181 | // } 182 | // } 183 | 184 | /** 185 | * Called to set a click listener for the given view holder. 186 | *

    187 | * The default implementation sets the click listener on the root view in the view holder. 188 | * If the root view isn't focusable this method should be overridden to set the listener 189 | * on the appropriate focusable child view(s). 190 | * 191 | * @param holder The view holder containing the view(s) on which the listener should be set. 192 | * @param listener The click listener to be set. 193 | */ 194 | public void setOnClickListener(ViewHolder holder, View.OnClickListener listener) { 195 | holder.view.setOnClickListener(listener); 196 | } 197 | 198 | @Override 199 | public final Object getFacet(Class facetClass) { 200 | if (mFacets == null) { 201 | return null; 202 | } 203 | return mFacets.get(facetClass); 204 | } 205 | 206 | /** 207 | * Sets dynamic implemented facet in addition to basic Presenter functions. 208 | * 209 | * @param facetClass Facet classes to query, can be class of {@link ItemAlignmentFacet}. 210 | * @param facetImpl Facet implementation. 211 | */ 212 | public final void setFacet(Class facetClass, Object facetImpl) { 213 | if (mFacets == null) { 214 | mFacets = new HashMap(); 215 | } 216 | mFacets.put(facetClass, facetImpl); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/bean/TabBean.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo.bean; 2 | 3 | import java.util.List; 4 | /** 5 | * @author: chenxin 6 | * @date: 2019-12-20 7 | * @email: chenxin7930@qq.com 8 | */ 9 | public class TabBean { 10 | 11 | 12 | /** 13 | * errcode : 0 14 | * description : 成功执行 15 | * result : [{"columnTitle":"我是标题~","columns":28,"rows":7,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":8,"h":7},{"x":22,"y":0,"w":6,"h":7}]},{"columns":10,"rows":3,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":2,"h":3},{"x":2,"y":0,"w":2,"h":3},{"x":4,"y":0,"w":2,"h":3},{"x":6,"y":0,"w":2,"h":3},{"x":8,"y":0,"w":2,"h":3}]},{"type":1,"columns":10,"rows":2,"horizontalLayoutList":[{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""}]},{"columnTitle":"我是Title","columns":28,"rows":7,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":7,"h":7},{"x":21,"y":0,"w":7,"h":7}]},{"columns":10,"rows":3,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":2,"h":3},{"x":2,"y":0,"w":2,"h":3},{"x":4,"y":0,"w":2,"h":3},{"x":6,"y":0,"w":2,"h":3},{"x":8,"y":0,"w":2,"h":3}]},{"columns":10,"rows":2,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":10,"h":2}]},{"columnTitle":"我是Title","columns":28,"rows":7,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":7,"h":7},{"x":21,"y":0,"w":7,"h":7}]},{"columns":10,"rows":3,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":2,"h":3},{"x":2,"y":0,"w":2,"h":3},{"x":4,"y":0,"w":2,"h":3},{"x":6,"y":0,"w":2,"h":3},{"x":8,"y":0,"w":2,"h":3}]},{"columns":10,"rows":2,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":10,"h":2}]},{"columnTitle":"我是Title","columns":28,"rows":7,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":7,"h":7},{"x":21,"y":0,"w":7,"h":7}]},{"columns":10,"rows":3,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":2,"h":3},{"x":2,"y":0,"w":2,"h":3},{"x":4,"y":0,"w":2,"h":3},{"x":6,"y":0,"w":2,"h":3},{"x":8,"y":0,"w":2,"h":3}]},{"columns":10,"rows":2,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":10,"h":2}]},{"columnTitle":"我是Title","columns":28,"rows":7,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":7,"h":7},{"x":21,"y":0,"w":7,"h":7}]},{"columns":10,"rows":3,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":2,"h":3},{"x":2,"y":0,"w":2,"h":3},{"x":4,"y":0,"w":2,"h":3},{"x":6,"y":0,"w":2,"h":3},{"x":8,"y":0,"w":2,"h":3}]},{"columns":10,"rows":2,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":10,"h":2}]},{"columnTitle":"我是Title","columns":28,"rows":7,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":7,"h":7},{"x":21,"y":0,"w":7,"h":7}]},{"columns":10,"rows":3,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":2,"h":3},{"x":2,"y":0,"w":2,"h":3},{"x":4,"y":0,"w":2,"h":3},{"x":6,"y":0,"w":2,"h":3},{"x":8,"y":0,"w":2,"h":3}]},{"columns":10,"rows":2,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":10,"h":2}]},{"columnTitle":"我是Title","columns":28,"rows":7,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":7,"h":7},{"x":21,"y":0,"w":7,"h":7}]},{"columns":10,"rows":3,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":2,"h":3},{"x":2,"y":0,"w":2,"h":3},{"x":4,"y":0,"w":2,"h":3},{"x":6,"y":0,"w":2,"h":3},{"x":8,"y":0,"w":2,"h":3}]},{"columns":10,"rows":2,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":10,"h":2}]},{"columnTitle":"我是Title","columns":28,"rows":7,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":7,"h":7},{"x":21,"y":0,"w":7,"h":7}]},{"columns":10,"rows":3,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":2,"h":3},{"x":2,"y":0,"w":2,"h":3},{"x":4,"y":0,"w":2,"h":3},{"x":6,"y":0,"w":2,"h":3},{"x":8,"y":0,"w":2,"h":3}]},{"columns":10,"rows":2,"absLayoutList":[{"posterUrl":"","x":0,"y":0,"w":10,"h":2}]}] 16 | */ 17 | 18 | private String errcode; 19 | private String description; 20 | private List result; 21 | 22 | public String getErrcode() { 23 | return errcode; 24 | } 25 | 26 | public void setErrcode(String errcode) { 27 | this.errcode = errcode; 28 | } 29 | 30 | public String getDescription() { 31 | return description; 32 | } 33 | 34 | public void setDescription(String description) { 35 | this.description = description; 36 | } 37 | 38 | public List getResult() { 39 | return result; 40 | } 41 | 42 | public void setResult(List result) { 43 | this.result = result; 44 | } 45 | 46 | public static class ResultBean { 47 | /** 48 | * columnTitle : 我是标题~ 49 | * columns : 28 50 | * rows : 7 51 | * absLayoutList : [{"posterUrl":"","x":0,"y":0,"w":14,"h":7},{"x":14,"y":0,"w":8,"h":7},{"x":22,"y":0,"w":6,"h":7}] 52 | * type : 1 53 | * horizontalLayoutList : [{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""},{"w":2,"h":2,"posterUrl":""}] 54 | */ 55 | public static final int TYPE_HORIZONTAL_LAYOUT = 1; 56 | public static final int TYPE_ABSOLUTE_LAYOUT = 0; 57 | private String columnTitle; 58 | private int columns; 59 | private int rows; 60 | // 1:水平滑动布局的数据,2:绝对布局的数据 61 | private int type; 62 | private List absLayoutList; 63 | private List horizontalLayoutList; 64 | 65 | public String getColumnTitle() { 66 | return columnTitle; 67 | } 68 | 69 | public void setColumnTitle(String columnTitle) { 70 | this.columnTitle = columnTitle; 71 | } 72 | 73 | public int getColumns() { 74 | return columns; 75 | } 76 | 77 | public void setColumns(int columns) { 78 | this.columns = columns; 79 | } 80 | 81 | public int getRows() { 82 | return rows; 83 | } 84 | 85 | public void setRows(int rows) { 86 | this.rows = rows; 87 | } 88 | 89 | public int getType() { 90 | return type; 91 | } 92 | 93 | public void setType(int type) { 94 | this.type = type; 95 | } 96 | 97 | public List getAbsLayoutList() { 98 | return absLayoutList; 99 | } 100 | 101 | public void setAbsLayoutList(List absLayoutList) { 102 | this.absLayoutList = absLayoutList; 103 | } 104 | 105 | public List getHorizontalLayoutList() { 106 | return horizontalLayoutList; 107 | } 108 | 109 | public void setHorizontalLayoutList(List horizontalLayoutList) { 110 | this.horizontalLayoutList = horizontalLayoutList; 111 | } 112 | 113 | public static class AbsLayoutListBean { 114 | /** 115 | * posterUrl : 116 | * x : 0 117 | * y : 0 118 | * w : 14 119 | * h : 7 120 | */ 121 | 122 | private String posterUrl; 123 | private int x; 124 | private int y; 125 | private int w; 126 | private int h; 127 | 128 | public String getPosterUrl() { 129 | return posterUrl; 130 | } 131 | 132 | public void setPosterUrl(String posterUrl) { 133 | this.posterUrl = posterUrl; 134 | } 135 | 136 | public int getX() { 137 | return x; 138 | } 139 | 140 | public void setX(int x) { 141 | this.x = x; 142 | } 143 | 144 | public int getY() { 145 | return y; 146 | } 147 | 148 | public void setY(int y) { 149 | this.y = y; 150 | } 151 | 152 | public int getW() { 153 | return w; 154 | } 155 | 156 | public void setW(int w) { 157 | this.w = w; 158 | } 159 | 160 | public int getH() { 161 | return h; 162 | } 163 | 164 | public void setH(int h) { 165 | this.h = h; 166 | } 167 | } 168 | 169 | public static class HorizontalLayoutListBean { 170 | /** 171 | * w : 2 172 | * h : 2 173 | * posterUrl : 174 | */ 175 | 176 | private int w; 177 | private int h; 178 | private String posterUrl; 179 | 180 | public int getW() { 181 | return w; 182 | } 183 | 184 | public void setW(int w) { 185 | this.w = w; 186 | } 187 | 188 | public int getH() { 189 | return h; 190 | } 191 | 192 | public void setH(int h) { 193 | this.h = h; 194 | } 195 | 196 | public String getPosterUrl() { 197 | return posterUrl; 198 | } 199 | 200 | public void setPosterUrl(String posterUrl) { 201 | this.posterUrl = posterUrl; 202 | } 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/ObjectAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.msisuzney.tv.waterfallayout.leanback; 15 | 16 | import android.database.Observable; 17 | 18 | /** 19 | * Base class adapter to be used in leanback activities. Provides access to a data model and is 20 | * decoupled from the presentation of the items via {@link PresenterSelector}. 21 | */ 22 | public abstract class ObjectAdapter { 23 | 24 | /** Indicates that an id has not been set. */ 25 | public static final int NO_ID = -1; 26 | 27 | /** 28 | * A DataObserver can be notified when an ObjectAdapter's underlying data 29 | * changes. Separate methods provide notifications about different types of 30 | * changes. 31 | */ 32 | public static abstract class DataObserver { 33 | /** 34 | * Called whenever the ObjectAdapter's data has changed in some manner 35 | * outside of the set of changes covered by the other range-based change 36 | * notification methods. 37 | */ 38 | public void onChanged() { 39 | } 40 | 41 | /** 42 | * Called when a range of items in the ObjectAdapter has changed. The 43 | * basic ordering and structure of the ObjectAdapter has not changed. 44 | * 45 | * @param positionStart The position of the first item that changed. 46 | * @param itemCount The number of items changed. 47 | */ 48 | public void onItemRangeChanged(int positionStart, int itemCount) { 49 | onChanged(); 50 | } 51 | 52 | /** 53 | * Called when a range of items is inserted into the ObjectAdapter. 54 | * 55 | * @param positionStart The position of the first inserted item. 56 | * @param itemCount The number of items inserted. 57 | */ 58 | public void onItemRangeInserted(int positionStart, int itemCount) { 59 | onChanged(); 60 | } 61 | 62 | /** 63 | * Called when a range of items is removed from the ObjectAdapter. 64 | * 65 | * @param positionStart The position of the first removed item. 66 | * @param itemCount The number of items removed. 67 | */ 68 | public void onItemRangeRemoved(int positionStart, int itemCount) { 69 | onChanged(); 70 | } 71 | } 72 | 73 | private static final class DataObservable extends Observable { 74 | 75 | DataObservable() { 76 | } 77 | 78 | public void notifyChanged() { 79 | for (int i = mObservers.size() - 1; i >= 0; i--) { 80 | mObservers.get(i).onChanged(); 81 | } 82 | } 83 | 84 | public void notifyItemRangeChanged(int positionStart, int itemCount) { 85 | for (int i = mObservers.size() - 1; i >= 0; i--) { 86 | mObservers.get(i).onItemRangeChanged(positionStart, itemCount); 87 | } 88 | } 89 | 90 | public void notifyItemRangeInserted(int positionStart, int itemCount) { 91 | for (int i = mObservers.size() - 1; i >= 0; i--) { 92 | mObservers.get(i).onItemRangeInserted(positionStart, itemCount); 93 | } 94 | } 95 | 96 | public void notifyItemRangeRemoved(int positionStart, int itemCount) { 97 | for (int i = mObservers.size() - 1; i >= 0; i--) { 98 | mObservers.get(i).onItemRangeRemoved(positionStart, itemCount); 99 | } 100 | } 101 | } 102 | 103 | private final DataObservable mObservable = new DataObservable(); 104 | private boolean mHasStableIds; 105 | private PresenterSelector mPresenterSelector; 106 | 107 | /** 108 | * Constructs an adapter with the given {@link PresenterSelector}. 109 | */ 110 | public ObjectAdapter(PresenterSelector presenterSelector) { 111 | setPresenterSelector(presenterSelector); 112 | } 113 | 114 | /** 115 | * Constructs an adapter that uses the given {@link Presenter} for all items. 116 | */ 117 | public ObjectAdapter(Presenter presenter) { 118 | setPresenterSelector(new SinglePresenterSelector(presenter)); 119 | } 120 | 121 | /** 122 | * Constructs an adapter. 123 | */ 124 | public ObjectAdapter() { 125 | } 126 | 127 | /** 128 | * Sets the presenter selector. May not be null. 129 | */ 130 | public final void setPresenterSelector(PresenterSelector presenterSelector) { 131 | if (presenterSelector == null) { 132 | throw new IllegalArgumentException("Presenter selector must not be null"); 133 | } 134 | final boolean update = (mPresenterSelector != null); 135 | final boolean selectorChanged = update && mPresenterSelector != presenterSelector; 136 | 137 | mPresenterSelector = presenterSelector; 138 | 139 | if (selectorChanged) { 140 | onPresenterSelectorChanged(); 141 | } 142 | if (update) { 143 | notifyChanged(); 144 | } 145 | } 146 | 147 | /** 148 | * Called when {@link #setPresenterSelector(PresenterSelector)} is called 149 | * and the PresenterSelector differs from the previous one. 150 | */ 151 | protected void onPresenterSelectorChanged() { 152 | } 153 | 154 | /** 155 | * Returns the presenter selector for this ObjectAdapter. 156 | */ 157 | public final PresenterSelector getPresenterSelector() { 158 | return mPresenterSelector; 159 | } 160 | 161 | /** 162 | * Registers a DataObserver for data change notifications. 163 | */ 164 | public final void registerObserver(DataObserver observer) { 165 | mObservable.registerObserver(observer); 166 | } 167 | 168 | /** 169 | * Unregisters a DataObserver for data change notifications. 170 | */ 171 | public final void unregisterObserver(DataObserver observer) { 172 | mObservable.unregisterObserver(observer); 173 | } 174 | 175 | /** 176 | * Unregisters all DataObservers for this ObjectAdapter. 177 | */ 178 | public final void unregisterAllObservers() { 179 | mObservable.unregisterAll(); 180 | } 181 | 182 | /** 183 | * Notifies UI that some items has changed. 184 | * 185 | * @param positionStart Starting position of the changed items. 186 | * @param itemCount Total number of items that changed. 187 | */ 188 | public final void notifyItemRangeChanged(int positionStart, int itemCount) { 189 | mObservable.notifyItemRangeChanged(positionStart, itemCount); 190 | } 191 | 192 | /** 193 | * Notifies UI that new items has been inserted. 194 | * 195 | * @param positionStart Position where new items has been inserted. 196 | * @param itemCount Count of the new items has been inserted. 197 | */ 198 | final protected void notifyItemRangeInserted(int positionStart, int itemCount) { 199 | mObservable.notifyItemRangeInserted(positionStart, itemCount); 200 | } 201 | 202 | /** 203 | * Notifies UI that some items that has been removed. 204 | * 205 | * @param positionStart Starting position of the removed items. 206 | * @param itemCount Total number of items that has been removed. 207 | */ 208 | final protected void notifyItemRangeRemoved(int positionStart, int itemCount) { 209 | mObservable.notifyItemRangeRemoved(positionStart, itemCount); 210 | } 211 | 212 | /** 213 | * Notifies UI that the underlying data has changed. 214 | */ 215 | final protected void notifyChanged() { 216 | mObservable.notifyChanged(); 217 | } 218 | 219 | /** 220 | * Returns true if the item ids are stable across changes to the 221 | * underlying data. When this is true, clients of the ObjectAdapter can use 222 | * {@link #getId(int)} to correlate Objects across changes. 223 | */ 224 | public final boolean hasStableIds() { 225 | return mHasStableIds; 226 | } 227 | 228 | /** 229 | * Sets whether the item ids are stable across changes to the underlying 230 | * data. 231 | */ 232 | public final void setHasStableIds(boolean hasStableIds) { 233 | boolean changed = mHasStableIds != hasStableIds; 234 | mHasStableIds = hasStableIds; 235 | 236 | if (changed) { 237 | onHasStableIdsChanged(); 238 | } 239 | } 240 | 241 | /** 242 | * Called when {@link #setHasStableIds(boolean)} is called and the status 243 | * of stable ids has changed. 244 | */ 245 | protected void onHasStableIdsChanged() { 246 | } 247 | 248 | /** 249 | * Returns the {@link Presenter} for the given item from the adapter. 250 | */ 251 | public final Presenter getPresenter(Object item) { 252 | if (mPresenterSelector == null) { 253 | throw new IllegalStateException("Presenter selector must not be null"); 254 | } 255 | return mPresenterSelector.getPresenter(item); 256 | } 257 | 258 | /** 259 | * Returns the number of items in the adapter. 260 | */ 261 | public abstract int size(); 262 | 263 | /** 264 | * Returns the item for the given position. 265 | */ 266 | public abstract Object get(int position); 267 | 268 | /** 269 | * Returns the id for the given position. 270 | */ 271 | public long getId(int position) { 272 | return NO_ID; 273 | } 274 | 275 | /** 276 | * Returns true if the adapter pairs each underlying data change with a call to notify and 277 | * false otherwise. 278 | */ 279 | public boolean isImmediateNotifySupported() { 280 | return false; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /demo/src/main/java/com/msisuzney/tv/demo/WaterfallFragment.java: -------------------------------------------------------------------------------- 1 | package com.msisuzney.tv.demo; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Bundle; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | 9 | import com.msisuzney.tv.demo.bean.ColumnFocusStateBean; 10 | import com.msisuzney.tv.demo.bean.FooterBean; 11 | import com.msisuzney.tv.demo.bean.RecyclerViewStateBean; 12 | import com.msisuzney.tv.demo.bean.TabBean; 13 | import com.msisuzney.tv.demo.bean.TitleBean; 14 | import com.msisuzney.tv.demo.viewfactory.ColumnItemViewFactory; 15 | import com.msisuzney.tv.demo.viewfactory.presenter.FooterViewPresenter; 16 | import com.msisuzney.tv.demo.viewfactory.presenter.TitlePresenter; 17 | import com.msisuzney.tv.waterfallayout.leanback.Presenter; 18 | import com.msisuzney.tv.waterfallayout.leanback.PresenterSelector; 19 | 20 | import androidx.recyclerview.widget.RecyclerView; 21 | 22 | import android.text.TextUtils; 23 | import android.view.KeyEvent; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.widget.Toast; 27 | 28 | import com.google.gson.Gson; 29 | import com.msisuzney.tv.waterfallayout.RowsFragment; 30 | import com.msisuzney.tv.waterfallayout.OnItemKeyListener; 31 | import com.msisuzney.tv.waterfallayout.StateChangeObservable; 32 | import com.msisuzney.tv.waterfallayout.model.ColumnLayoutCollection; 33 | import com.msisuzney.tv.waterfallayout.model.ColumnLayoutItem; 34 | import com.msisuzney.tv.waterfallayout.model.HorizontalLayoutCollection; 35 | import com.msisuzney.tv.waterfallayout.model.HorizontalLayoutItem; 36 | 37 | import java.io.IOException; 38 | import java.io.InputStream; 39 | import java.io.InputStreamReader; 40 | import java.lang.ref.WeakReference; 41 | import java.util.ArrayList; 42 | import java.util.List; 43 | 44 | /** 45 | * @author: chenxin 46 | * @date: 2019-12-20 47 | * @email: chenxin7930@qq.com 48 | */ 49 | public class WaterfallFragment extends RowsFragment implements OnItemKeyListener { 50 | 51 | //运营位间距 = FOCUS_PADDING * 2(焦点的预留位置)+ COLUMN_ITEM_PADDING * 2 = 48 52 | public static int COLUMN_ITEM_PADDING = 10; 53 | // public static int COLUMN_TITLE_HEIGHT = 100; 54 | //行左右的margin,实际要扣除运营位间距 55 | public static int COLUMN_LEFT_RIGHT_MARGIN = 120 - COLUMN_ITEM_PADDING; 56 | //title布局,没有COLUMN_ITEM_PADDING 57 | // public static int COLUMN_LEFT_RIGHT_MARGIN2 = 120; 58 | public static int COLUMN_WIDTH = 1920 - 2 * COLUMN_LEFT_RIGHT_MARGIN; // = 1728 59 | 60 | private MyStateChangeObservable observable; 61 | private FooterViewPresenter footerViewPresenter = new FooterViewPresenter(); 62 | private TitlePresenter titleViewPresenter = new TitlePresenter(); 63 | 64 | @Override 65 | public PresenterSelector initBlockPresenterSelector() { 66 | return new ColumnItemViewFactory(observable, this); 67 | } 68 | 69 | 70 | @Override 71 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 72 | super.onViewCreated(view, savedInstanceState); 73 | addOnScrollListener(new RecyclerView.OnScrollListener() { 74 | @Override 75 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { 76 | // if (newState == RecyclerView.SCROLL_STATE_IDLE) { 77 | // GlideApp.with(getContext()).resumeRequests(); 78 | // } else { 79 | // GlideApp.with(getContext()).pauseRequests(); 80 | // } 81 | } 82 | }); 83 | try { 84 | new LoadDataAsyncTask(this).execute(getContext().getAssets().open("data.json")); 85 | } catch (IOException e) { 86 | e.printStackTrace(); 87 | } 88 | } 89 | 90 | @Override 91 | protected PresenterSelector initOtherPresenterSelector() { 92 | return new PresenterSelector() { 93 | @Override 94 | public Presenter getPresenter(Object item) { 95 | if (item instanceof FooterBean) { 96 | return footerViewPresenter; 97 | } else if (item instanceof TitleBean) { 98 | return titleViewPresenter; 99 | } 100 | return null; 101 | } 102 | }; 103 | } 104 | 105 | @Override 106 | protected StateChangeObservable initStateChangeObservable() { 107 | observable = new MyStateChangeObservable(); 108 | return observable; 109 | } 110 | 111 | @Override 112 | public void onResume() { 113 | super.onResume(); 114 | } 115 | 116 | 117 | @Override 118 | public void onClick(Object item) { 119 | Toast.makeText(getContext().getApplicationContext(), "click", Toast.LENGTH_SHORT).show(); 120 | } 121 | 122 | @Override 123 | public boolean onKey(View v, KeyEvent event, Object item) { 124 | return false; 125 | } 126 | 127 | private void updateData(TabBean tabBean) { 128 | /** 129 | 1.演示按照屏幕宽度作为固定的尺寸,计算栏目中所有View的实际像素宽高。TabBean中宽高的是比例 130 | */ 131 | List rows = new ArrayList<>(); 132 | for (int i = 0; i < tabBean.getResult().size(); i++) { 133 | TabBean.ResultBean tabColumn = tabBean.getResult().get(i); 134 | if (!TextUtils.isEmpty(tabColumn.getColumnTitle())) {//有标题,添加一个标题bean 135 | rows.add(new TitleBean(tabColumn.getColumnTitle())); 136 | } 137 | if (tabColumn.getType() == TabBean.ResultBean.TYPE_HORIZONTAL_LAYOUT) {//是水平滑动布局的栏目 138 | //宽度固定,根据宽度的比例计算高度 139 | float gridWH = (float) (COLUMN_WIDTH * 1.0 / tabColumn.getColumns()); 140 | int height = (int) (gridWH * tabColumn.getRows()); 141 | HorizontalLayoutCollection horizontalLayoutCollection = new HorizontalLayoutCollection(ViewGroup.LayoutParams.MATCH_PARENT, height); 142 | List items = new ArrayList<>(); 143 | for (int j = 0; j < tabColumn.getHorizontalLayoutList().size(); j++) { 144 | HorizontalLayoutItem item = new HorizontalLayoutItem(); 145 | TabBean.ResultBean.HorizontalLayoutListBean bean = tabColumn.getHorizontalLayoutList().get(j); 146 | int w = (int) (gridWH * bean.getW()); 147 | int h = (int) (gridWH * bean.getH()); 148 | item.setHeight(h); 149 | item.setWidth(w); 150 | item.setData(bean); 151 | items.add(item); 152 | } 153 | horizontalLayoutCollection.setItems(items); 154 | rows.add(horizontalLayoutCollection); 155 | } else if (tabColumn.getType() == TabBean.ResultBean.TYPE_ABSOLUTE_LAYOUT) { //是绝对布局的栏目,计算每行中每个运营位的绝对位置 156 | List items = new ArrayList<>(); 157 | //网格的实际宽高 158 | float gridWH = (float) (COLUMN_WIDTH * 1.0 / tabColumn.getColumns()); 159 | int height = (int) (gridWH * tabColumn.getRows()); 160 | ColumnLayoutCollection cLayoutCollection = new ColumnLayoutCollection(ViewGroup.LayoutParams.MATCH_PARENT, height); 161 | for (int j = 0; j < tabColumn.getAbsLayoutList().size(); j++) { 162 | TabBean.ResultBean.AbsLayoutListBean block = tabColumn.getAbsLayoutList().get(j); 163 | int x = (int) (gridWH * block.getX()); 164 | int y = (int) (gridWH * block.getY()); 165 | int w = (int) (gridWH * (block.getW())); 166 | int h = (int) (gridWH * (block.getH())); 167 | x += COLUMN_LEFT_RIGHT_MARGIN; 168 | ColumnLayoutItem item = new ColumnLayoutItem(); 169 | item.setX(x); 170 | item.setY(y); 171 | item.setWidth(w); 172 | item.setHeight(h); 173 | item.setData(block); 174 | items.add(item); 175 | } 176 | cLayoutCollection.setItems(items); 177 | rows.add(cLayoutCollection); 178 | } 179 | } 180 | /** 181 | 2. 演示栏目中的View监听布局的状态 182 | */ 183 | 184 | ColumnLayoutCollection c2Collection = new ColumnLayoutCollection(ViewGroup.LayoutParams.MATCH_PARENT, 200); 185 | List items = new ArrayList<>(); 186 | ColumnLayoutItem item = new ColumnLayoutItem(); 187 | item.setHeight(ViewGroup.LayoutParams.MATCH_PARENT); 188 | item.setWidth(ViewGroup.LayoutParams.MATCH_PARENT); 189 | item.setData(new RecyclerViewStateBean()); 190 | items.add(item); 191 | c2Collection.setItems(items); 192 | rows.add(6, new TitleBean("栏目中的View监听状态")); 193 | rows.add(7, c2Collection); 194 | 195 | /** 196 | 3. 演示栏目中的View监听栏目是否获得焦点 197 | */ 198 | 199 | List myitems = new ArrayList<>(); 200 | //普通View 201 | ColumnLayoutItem normalItem = new ColumnLayoutItem(); 202 | normalItem.setHeight(200); 203 | normalItem.setX(700); 204 | normalItem.setY(0); 205 | normalItem.setWidth(400); 206 | normalItem.setData(new TabBean.ResultBean.AbsLayoutListBean()); 207 | myitems.add(normalItem); 208 | 209 | //ColumnFocusChangeListenerTextView 210 | ColumnLayoutItem myitem = new ColumnLayoutItem(); 211 | myitem.setX(100); 212 | myitem.setY(30); 213 | myitem.setHeight(100); 214 | myitem.setWidth(500); 215 | myitem.setData(new ColumnFocusStateBean()); 216 | myitems.add(myitem); 217 | 218 | ColumnLayoutCollection c3Collection = new ColumnLayoutCollection(ViewGroup.LayoutParams.MATCH_PARENT, 200); 219 | c3Collection.setItems(myitems); 220 | rows.add(8, new TitleBean("栏目中的View监听栏目的焦点状态")); 221 | rows.add(9, c3Collection); 222 | 223 | 224 | postRefreshRunnable(() -> { 225 | if (isAdded()) { 226 | int size = size(); 227 | addAll(size, rows); 228 | add(new FooterBean()); 229 | } 230 | }); 231 | } 232 | 233 | 234 | private static class LoadDataAsyncTask extends AsyncTask { 235 | 236 | private WeakReference ref; 237 | 238 | public LoadDataAsyncTask(WaterfallFragment waterfallFragment) { 239 | super(); 240 | ref = new WeakReference(waterfallFragment); 241 | } 242 | 243 | @Override 244 | protected TabBean doInBackground(InputStream... inputStreams) { 245 | try { 246 | InputStreamReader inputStreamReader = new InputStreamReader(inputStreams[0]); 247 | TabBean tabBean = new Gson().fromJson(inputStreamReader, TabBean.class); 248 | return tabBean; 249 | } catch (Exception e) { 250 | e.printStackTrace(); 251 | } 252 | return null; 253 | } 254 | 255 | @Override 256 | protected void onPostExecute(TabBean tabBean) { 257 | super.onPostExecute(tabBean); 258 | WaterfallFragment wf = ref.get(); 259 | if (wf != null && tabBean != null) { 260 | wf.updateData(tabBean); 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /waterfallayout/src/main/java/com/msisuzney/tv/waterfallayout/leanback/WindowAlignment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package com.msisuzney.tv.waterfallayout.leanback; 16 | 17 | import static com.msisuzney.tv.waterfallayout.leanback.BaseGridView.WINDOW_ALIGN_BOTH_EDGE; 18 | import static com.msisuzney.tv.waterfallayout.leanback.BaseGridView.WINDOW_ALIGN_HIGH_EDGE; 19 | import static com.msisuzney.tv.waterfallayout.leanback.BaseGridView.WINDOW_ALIGN_LOW_EDGE; 20 | import static com.msisuzney.tv.waterfallayout.leanback.BaseGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED; 21 | import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL; 22 | 23 | /** 24 | * Maintains Window Alignment information of two axis. 25 | */ 26 | class WindowAlignment { 27 | 28 | /** 29 | * Maintains alignment information in one direction. 30 | */ 31 | public static class Axis { 32 | /** 33 | * mScrollCenter is used to calculate dynamic transformation based on how far a view 34 | * is from the mScrollCenter. For example, the views with center close to mScrollCenter 35 | * will be scaled up. 36 | */ 37 | private float mScrollCenter; 38 | /** 39 | * Right or bottom edge of last child. 40 | */ 41 | private int mMaxEdge; 42 | /** 43 | * Left or top edge of first child, typically should be zero. 44 | */ 45 | private int mMinEdge; 46 | /** 47 | * Max Scroll value 48 | */ 49 | private int mMaxScroll; 50 | /** 51 | * Min Scroll value 52 | */ 53 | private int mMinScroll; 54 | 55 | private int mWindowAlignment = WINDOW_ALIGN_BOTH_EDGE; 56 | 57 | private int mWindowAlignmentOffset = 0; 58 | 59 | private float mWindowAlignmentOffsetPercent = 50f; 60 | 61 | private int mSize; 62 | 63 | private int mPaddingLow; 64 | 65 | private int mPaddingHigh; 66 | 67 | private boolean mReversedFlow; 68 | 69 | private String mName; // for debugging 70 | 71 | public Axis(String name) { 72 | reset(); 73 | mName = name; 74 | } 75 | 76 | final public int getWindowAlignment() { 77 | return mWindowAlignment; 78 | } 79 | 80 | final public void setWindowAlignment(int windowAlignment) { 81 | mWindowAlignment = windowAlignment; 82 | } 83 | 84 | final public int getWindowAlignmentOffset() { 85 | return mWindowAlignmentOffset; 86 | } 87 | 88 | final public void setWindowAlignmentOffset(int offset) { 89 | mWindowAlignmentOffset = offset; 90 | } 91 | 92 | final public void setWindowAlignmentOffsetPercent(float percent) { 93 | if ((percent < 0 || percent > 100) 94 | && percent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) { 95 | throw new IllegalArgumentException(); 96 | } 97 | mWindowAlignmentOffsetPercent = percent; 98 | } 99 | 100 | final public float getWindowAlignmentOffsetPercent() { 101 | return mWindowAlignmentOffsetPercent; 102 | } 103 | 104 | final public int getScrollCenter() { 105 | return (int) mScrollCenter; 106 | } 107 | 108 | /** set minEdge, Integer.MIN_VALUE means unknown*/ 109 | final public void setMinEdge(int minEdge) { 110 | mMinEdge = minEdge; 111 | } 112 | 113 | final public int getMinEdge() { 114 | return mMinEdge; 115 | } 116 | 117 | /** set minScroll, Integer.MIN_VALUE means unknown*/ 118 | final public void setMinScroll(int minScroll) { 119 | mMinScroll = minScroll; 120 | } 121 | 122 | final public int getMinScroll() { 123 | return mMinScroll; 124 | } 125 | 126 | final public void invalidateScrollMin() { 127 | mMinEdge = Integer.MIN_VALUE; 128 | mMinScroll = Integer.MIN_VALUE; 129 | } 130 | 131 | /** update max edge, Integer.MAX_VALUE means unknown*/ 132 | final public void setMaxEdge(int maxEdge) { 133 | mMaxEdge = maxEdge; 134 | } 135 | 136 | final public int getMaxEdge() { 137 | return mMaxEdge; 138 | } 139 | 140 | /** update max scroll, Integer.MAX_VALUE means unknown*/ 141 | final public void setMaxScroll(int maxScroll) { 142 | mMaxScroll = maxScroll; 143 | } 144 | 145 | final public int getMaxScroll() { 146 | return mMaxScroll; 147 | } 148 | 149 | final public void invalidateScrollMax() { 150 | mMaxEdge = Integer.MAX_VALUE; 151 | mMaxScroll = Integer.MAX_VALUE; 152 | } 153 | 154 | final public float updateScrollCenter(float scrollTarget) { 155 | mScrollCenter = scrollTarget; 156 | return scrollTarget; 157 | } 158 | 159 | void reset() { 160 | mScrollCenter = Integer.MIN_VALUE; 161 | mMinEdge = Integer.MIN_VALUE; 162 | mMaxEdge = Integer.MAX_VALUE; 163 | } 164 | 165 | final public boolean isMinUnknown() { 166 | return mMinEdge == Integer.MIN_VALUE; 167 | } 168 | 169 | final public boolean isMaxUnknown() { 170 | return mMaxEdge == Integer.MAX_VALUE; 171 | } 172 | 173 | final public void setSize(int size) { 174 | mSize = size; 175 | } 176 | 177 | final public int getSize() { 178 | return mSize; 179 | } 180 | 181 | final public void setPadding(int paddingLow, int paddingHigh) { 182 | mPaddingLow = paddingLow; 183 | mPaddingHigh = paddingHigh; 184 | } 185 | 186 | final public int getPaddingLow() { 187 | return mPaddingLow; 188 | } 189 | 190 | final public int getPaddingHigh() { 191 | return mPaddingHigh; 192 | } 193 | 194 | final public int getClientSize() { 195 | return mSize - mPaddingLow - mPaddingHigh; 196 | } 197 | 198 | final public int getSystemScrollPos(boolean isAtMin, boolean isAtMax) { 199 | return getSystemScrollPos((int) mScrollCenter, isAtMin, isAtMax); 200 | } 201 | 202 | final public int getSystemScrollPos(int scrollCenter, boolean isAtMin, boolean isAtMax) { 203 | int middlePosition; 204 | if (!mReversedFlow) { 205 | if (mWindowAlignmentOffset >= 0) { 206 | middlePosition = mWindowAlignmentOffset - mPaddingLow; 207 | } else { 208 | middlePosition = mSize + mWindowAlignmentOffset - mPaddingLow; 209 | } 210 | if (mWindowAlignmentOffsetPercent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) { 211 | middlePosition += (int) (mSize * mWindowAlignmentOffsetPercent / 100); 212 | } 213 | } else { 214 | if (mWindowAlignmentOffset >= 0) { 215 | middlePosition = mSize - mWindowAlignmentOffset - mPaddingLow; 216 | } else { 217 | middlePosition = - mWindowAlignmentOffset - mPaddingLow; 218 | } 219 | if (mWindowAlignmentOffsetPercent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) { 220 | middlePosition -= (int) (mSize * mWindowAlignmentOffsetPercent / 100); 221 | } 222 | } 223 | int clientSize = getClientSize(); 224 | int afterMiddlePosition = clientSize - middlePosition; 225 | boolean isMinUnknown = isMinUnknown(); 226 | boolean isMaxUnknown = isMaxUnknown(); 227 | if (!isMinUnknown && !isMaxUnknown 228 | && (mWindowAlignment & WINDOW_ALIGN_BOTH_EDGE) == WINDOW_ALIGN_BOTH_EDGE) { 229 | if (mMaxEdge - mMinEdge <= clientSize) { 230 | // total children size is less than view port and we want to align 231 | // both edge: align first child to start edge of view port 232 | return mReversedFlow ? mMaxEdge - mPaddingLow - clientSize 233 | : mMinEdge - mPaddingLow; 234 | } 235 | } 236 | if (!isMinUnknown) { 237 | if ((!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0 238 | : (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0) 239 | && (isAtMin || scrollCenter - mMinEdge <= middlePosition)) { 240 | // scroll center is within half of view port size: align the start edge 241 | // of first child to the start edge of view port 242 | return mMinEdge - mPaddingLow; 243 | } 244 | } 245 | if (!isMaxUnknown) { 246 | if ((!mReversedFlow ? (mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0 247 | : (mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0) 248 | && (isAtMax || mMaxEdge - scrollCenter <= afterMiddlePosition)) { 249 | // scroll center is very close to the end edge of view port : align the 250 | // end edge of last children (plus expanded size) to view port's end 251 | return mMaxEdge - mPaddingLow - clientSize; 252 | } 253 | } 254 | // else put scroll center in middle of view port 255 | return scrollCenter - middlePosition - mPaddingLow; 256 | } 257 | 258 | final public void setReversedFlow(boolean reversedFlow) { 259 | mReversedFlow = reversedFlow; 260 | } 261 | 262 | @Override 263 | public String toString() { 264 | return "center: " + mScrollCenter + " min:" + mMinEdge + " max:" + mMaxEdge; 265 | } 266 | 267 | } 268 | 269 | private int mOrientation = HORIZONTAL; 270 | 271 | final public Axis vertical = new Axis("vertical"); 272 | 273 | final public Axis horizontal = new Axis("horizontal"); 274 | 275 | private Axis mMainAxis = horizontal; 276 | 277 | private Axis mSecondAxis = vertical; 278 | 279 | final public Axis mainAxis() { 280 | return mMainAxis; 281 | } 282 | 283 | final public Axis secondAxis() { 284 | return mSecondAxis; 285 | } 286 | 287 | final public void setOrientation(int orientation) { 288 | mOrientation = orientation; 289 | if (mOrientation == HORIZONTAL) { 290 | mMainAxis = horizontal; 291 | mSecondAxis = vertical; 292 | } else { 293 | mMainAxis = vertical; 294 | mSecondAxis = horizontal; 295 | } 296 | } 297 | 298 | final public int getOrientation() { 299 | return mOrientation; 300 | } 301 | 302 | final public void reset() { 303 | mainAxis().reset(); 304 | } 305 | 306 | @Override 307 | public String toString() { 308 | return new StringBuffer().append("horizontal=") 309 | .append(horizontal.toString()) 310 | .append("; vertical=") 311 | .append(vertical.toString()) 312 | .toString(); 313 | } 314 | 315 | } 316 | --------------------------------------------------------------------------------