├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── main ├── AndroidManifest.xml ├── java └── org │ └── lucasr │ └── layoutsamples │ ├── adapter │ ├── Tweet.java │ ├── TweetPresenter.java │ └── TweetsAdapter.java │ ├── app │ ├── App.java │ ├── MainActivity.java │ ├── TweetsFragment.java │ └── TweetsListView.java │ ├── async │ ├── AsyncTweetElement.java │ ├── AsyncTweetElementFactory.java │ ├── AsyncTweetView.java │ ├── HeadlessElementHost.java │ ├── TweetsLayoutLoader.java │ └── UIElementCache.java │ ├── canvas │ ├── AbstractUIElement.java │ ├── ImageElement.java │ ├── StaticLayoutWithMaxLines.java │ ├── TextElement.java │ ├── UIElement.java │ ├── UIElementGroup.java │ ├── UIElementHost.java │ ├── UIElementInflater.java │ ├── UIElementView.java │ └── UIElementWrapper.java │ ├── util │ ├── ImageUtils.java │ ├── RawResource.java │ └── ViewServer.java │ └── widget │ ├── ImageElementTarget.java │ ├── TweetCompositeView.java │ ├── TweetElement.java │ ├── TweetElementView.java │ └── TweetLayoutView.java └── res ├── drawable-hdpi └── ic_launcher.png ├── drawable-mdpi ├── ic_launcher.png ├── tweet_favourite.png ├── tweet_reply.png └── tweet_retweet.png ├── drawable-xhdpi └── ic_launcher.png ├── drawable-xxhdpi └── ic_launcher.png ├── layout ├── activity_main.xml ├── fragment_tweets.xml ├── tweet_async_row.xml ├── tweet_composite_row.xml ├── tweet_composite_view.xml ├── tweet_element_row.xml ├── tweet_element_view.xml ├── tweet_layout_row.xml └── tweet_layout_view.xml ├── raw └── tweets.json └── values ├── attrs.xml ├── colors.xml ├── dimens.xml ├── strings.xml └── themes.xml /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .gradle 3 | .idea 4 | *.iml 5 | local.properties 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | android-layout-samples 2 | ====================== 3 | 4 | Explorations around Android custom layouts, including off main thread View measure/layout passes. 5 | 6 | Sample code for: 7 | * Composite View 8 | * Custom Composite View 9 | * Flat Custom View 10 | * Async Custom View 11 | 12 | For more information, read: http://lucasr.org/?p=3920 13 | 14 | License 15 | ======= 16 | 17 | Copyright 2014 Lucas Rocha 18 | 19 | Licensed under the Apache License, Version 2.0 (the "License"); 20 | you may not use this file except in compliance with the License. 21 | You may obtain a copy of the License at 22 | 23 | http://www.apache.org/licenses/LICENSE-2.0 24 | 25 | Unless required by applicable law or agreed to in writing, software 26 | distributed under the License is distributed on an "AS IS" BASIS, 27 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | See the License for the specific language governing permissions and 29 | limitations under the License. 30 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.0.0' 7 | } 8 | } 9 | 10 | allprojects { 11 | repositories { 12 | mavenCentral() 13 | } 14 | } 15 | 16 | apply plugin: 'android' 17 | 18 | android { 19 | compileSdkVersion 19 20 | buildToolsVersion "21.1.2" 21 | 22 | defaultConfig { 23 | minSdkVersion 14 24 | targetSdkVersion 19 25 | versionCode 1 26 | versionName "1.0" 27 | } 28 | } 29 | 30 | dependencies { 31 | compile fileTree(dir: 'libs', include: ['*.jar']) 32 | compile 'com.android.support:support-v4:21.0.+' 33 | compile 'com.squareup.picasso:picasso:2.2.0' 34 | compile 'org.lucasr.smoothie:smoothie:0.1.0' 35 | } 36 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/android-layout-samples/d0fa72fd0fe46788d514db9432e20795e0d2ee25/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jan 31 22:57:10 GMT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/adapter/Tweet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.adapter; 18 | 19 | import org.json.JSONException; 20 | import org.json.JSONObject; 21 | 22 | public class Tweet { 23 | private static final String JSON_ID = "id"; 24 | private static final String JSON_AUTHOR_NAME = "authorName"; 25 | private static final String JSON_MESSAGE = "message"; 26 | private static final String JSON_PROFILE_IMAGE_URL = "profileImageUrl"; 27 | private static final String JSON_POST_IMAGE_URL = "postImageUrl"; 28 | 29 | private final long mId; 30 | private final String mAuthorName; 31 | private final String mMessage; 32 | private final String mProfileImageUrl; 33 | private final String mPostImageUrl; 34 | 35 | public Tweet(JSONObject jsonTweet) throws JSONException { 36 | mId = jsonTweet.getLong(JSON_ID); 37 | mMessage = jsonTweet.getString(JSON_MESSAGE); 38 | mAuthorName = jsonTweet.getString(JSON_AUTHOR_NAME); 39 | mProfileImageUrl = jsonTweet.getString(JSON_PROFILE_IMAGE_URL); 40 | mPostImageUrl = jsonTweet.optString(JSON_POST_IMAGE_URL, null); 41 | } 42 | 43 | public long getId() { 44 | return mId; 45 | } 46 | 47 | public String getMessage() { 48 | return mMessage; 49 | } 50 | 51 | public String getAuthorName() { 52 | return mAuthorName; 53 | } 54 | 55 | public String getProfileImageUrl() { 56 | return mProfileImageUrl; 57 | } 58 | 59 | public String getPostImageUrl() { 60 | return mPostImageUrl; 61 | } 62 | 63 | @Override 64 | public boolean equals(Object o) { 65 | if (this == o) { 66 | return true; 67 | } 68 | 69 | if (!(o instanceof Tweet)) { 70 | return false; 71 | } 72 | 73 | Tweet other = (Tweet) o; 74 | return (mId == other.mId); 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | return "Tweet@" + mId; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/adapter/TweetPresenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.adapter; 18 | 19 | import android.graphics.drawable.Drawable; 20 | 21 | import org.lucasr.layoutsamples.adapter.Tweet; 22 | 23 | import java.util.EnumSet; 24 | 25 | public interface TweetPresenter { 26 | public enum UpdateFlags { 27 | NO_IMAGE_LOADING 28 | } 29 | 30 | public enum Action { 31 | REPLY, 32 | RETWEET, 33 | FAVOURITE 34 | } 35 | 36 | public void update(Tweet tweet, EnumSet flags); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/adapter/TweetsAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.adapter; 18 | 19 | import android.content.Context; 20 | import android.view.LayoutInflater; 21 | import android.view.View; 22 | import android.view.ViewGroup; 23 | import android.widget.BaseAdapter; 24 | 25 | import org.json.JSONArray; 26 | import org.json.JSONObject; 27 | import org.lucasr.layoutsamples.app.R; 28 | import org.lucasr.layoutsamples.util.RawResource; 29 | 30 | import java.util.ArrayList; 31 | import java.util.EnumSet; 32 | import java.util.List; 33 | 34 | public class TweetsAdapter extends BaseAdapter { 35 | private final Context mContext; 36 | private int mPresenterId; 37 | 38 | private static List sEntries; 39 | 40 | public TweetsAdapter(Context context, int presenterId) { 41 | mContext = context; 42 | mPresenterId = presenterId; 43 | loadFromResource(R.raw.tweets); 44 | } 45 | 46 | private void loadFromResource(int resID) { 47 | if (sEntries != null) { 48 | return; 49 | } 50 | 51 | try { 52 | final JSONArray tweets = RawResource.getAsJSON(mContext, resID); 53 | sEntries = new ArrayList(tweets.length()); 54 | 55 | final int count = tweets.length(); 56 | for (int i = 0; i < count; i++) { 57 | final JSONObject tweet = (JSONObject) tweets.get(i); 58 | sEntries.add(new Tweet(tweet)); 59 | } 60 | } catch (Exception e) { 61 | e.printStackTrace(); 62 | } 63 | } 64 | 65 | @Override 66 | public int getCount() { 67 | return sEntries.size(); 68 | } 69 | 70 | @Override 71 | public Object getItem(int position) { 72 | return sEntries.get(position); 73 | } 74 | 75 | @Override 76 | public View getView(int position, View convertView, ViewGroup parent) { 77 | final TweetPresenter presenter; 78 | if (convertView == null) { 79 | presenter = (TweetPresenter) LayoutInflater.from(mContext).inflate(mPresenterId, parent, false); 80 | } else { 81 | presenter = (TweetPresenter) convertView; 82 | } 83 | 84 | Tweet tweet = (Tweet) getItem(position); 85 | presenter.update(tweet, EnumSet.noneOf(TweetPresenter.UpdateFlags.class)); 86 | 87 | return (View) presenter; 88 | } 89 | 90 | @Override 91 | public long getItemId(int position) { 92 | return sEntries.get(position).getId(); 93 | } 94 | 95 | @Override 96 | public boolean hasStableIds() { 97 | return true; 98 | } 99 | 100 | public void setPresenter(int id) { 101 | mPresenterId = id; 102 | notifyDataSetChanged(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/app/App.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.app; 18 | 19 | import android.app.Application; 20 | import android.content.Context; 21 | 22 | import org.lucasr.layoutsamples.async.UIElementCache; 23 | 24 | public class App extends Application { 25 | private UIElementCache mElementCache; 26 | 27 | @Override 28 | public void onCreate() { 29 | super.onCreate(); 30 | mElementCache = new UIElementCache(); 31 | } 32 | 33 | public UIElementCache getElementCache() { 34 | return mElementCache; 35 | } 36 | 37 | public static App getInstance(Context context) { 38 | return (App) context.getApplicationContext(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.app; 18 | 19 | import java.util.Locale; 20 | 21 | import android.os.Bundle; 22 | import android.support.v4.app.Fragment; 23 | import android.support.v4.app.FragmentManager; 24 | import android.support.v4.app.FragmentPagerAdapter; 25 | import android.support.v4.app.FragmentActivity; 26 | import android.support.v4.view.ViewPager; 27 | 28 | import org.lucasr.layoutsamples.util.ViewServer; 29 | 30 | public class MainActivity extends FragmentActivity { 31 | private LayoutsAdapter mPagerAdapter; 32 | private ViewPager mViewPager; 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | 39 | mPagerAdapter = new LayoutsAdapter(getSupportFragmentManager()); 40 | mViewPager = (ViewPager) findViewById(R.id.pager); 41 | mViewPager.setAdapter(mPagerAdapter); 42 | 43 | ViewServer.get(this).addWindow(this); 44 | } 45 | 46 | @Override 47 | public void onDestroy() { 48 | super.onDestroy(); 49 | ViewServer.get(this).removeWindow(this); 50 | } 51 | 52 | @Override 53 | public void onResume() { 54 | super.onResume(); 55 | ViewServer.get(this).setFocusedWindow(this); 56 | } 57 | 58 | private class LayoutsAdapter extends FragmentPagerAdapter { 59 | public LayoutsAdapter(FragmentManager fm) { 60 | super(fm); 61 | } 62 | 63 | @Override 64 | public Fragment getItem(int position) { 65 | int presenterId = 0; 66 | switch (position) { 67 | case 0: 68 | presenterId = R.layout.tweet_composite_row; 69 | break; 70 | 71 | case 1: 72 | presenterId = R.layout.tweet_layout_row; 73 | break; 74 | 75 | case 2: 76 | presenterId = R.layout.tweet_element_row; 77 | break; 78 | 79 | case 3: 80 | presenterId = R.layout.tweet_async_row; 81 | break; 82 | } 83 | 84 | return TweetsFragment.newInstance(presenterId); 85 | } 86 | 87 | @Override 88 | public int getCount() { 89 | return 4; 90 | } 91 | 92 | @Override 93 | public CharSequence getPageTitle(int position) { 94 | final Locale locale = Locale.getDefault(); 95 | 96 | switch (position) { 97 | case 0: 98 | return getString(R.string.title_composite).toUpperCase(locale); 99 | 100 | case 1: 101 | return getString(R.string.title_layout).toUpperCase(locale); 102 | 103 | case 2: 104 | return getString(R.string.title_element).toUpperCase(locale); 105 | 106 | case 3: 107 | return getString(R.string.title_async).toUpperCase(locale); 108 | } 109 | 110 | return null; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/app/TweetsFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.app; 18 | 19 | import android.os.Bundle; 20 | import android.support.v4.app.Fragment; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | 25 | public class TweetsFragment extends Fragment { 26 | private static final String ARG_PRESENTER_ID = "presenter_id"; 27 | 28 | public static TweetsFragment newInstance(int presenterId) { 29 | TweetsFragment fragment = new TweetsFragment(); 30 | 31 | Bundle args = new Bundle(); 32 | args.putInt(ARG_PRESENTER_ID, presenterId); 33 | fragment.setArguments(args); 34 | 35 | return fragment; 36 | } 37 | 38 | @Override 39 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 40 | Bundle savedInstanceState) { 41 | return inflater.inflate(R.layout.fragment_tweets, container, false); 42 | } 43 | 44 | @Override 45 | public void onViewCreated(View view, Bundle savedInstanceState) { 46 | super.onViewCreated(view, savedInstanceState); 47 | 48 | TweetsListView list = (TweetsListView) getView().findViewById(R.id.list); 49 | int presenterId = getArguments().getInt(ARG_PRESENTER_ID); 50 | list.setPresenter(presenterId); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/app/TweetsListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.app; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.ViewTreeObserver.OnGlobalLayoutListener; 22 | 23 | import org.lucasr.layoutsamples.async.TweetsLayoutLoader; 24 | import org.lucasr.layoutsamples.adapter.TweetsAdapter; 25 | import org.lucasr.layoutsamples.async.AsyncTweetElementFactory; 26 | import org.lucasr.smoothie.AsyncListView; 27 | import org.lucasr.smoothie.ItemManager; 28 | 29 | public class TweetsListView extends AsyncListView { 30 | private TweetsAdapter mTweetsAdapter; 31 | private int mPresenterId; 32 | 33 | public TweetsListView(Context context, AttributeSet attrs) { 34 | this(context, attrs, android.R.attr.listViewStyle); 35 | } 36 | 37 | public TweetsListView(Context context, AttributeSet attrs, int defStyle) { 38 | super(context, attrs, defStyle); 39 | mPresenterId = R.layout.tweet_composite_row; 40 | } 41 | 42 | private void updateTargetWidth() { 43 | if (mPresenterId != R.layout.tweet_async_row) { 44 | return; 45 | } 46 | 47 | final Context context = getContext(); 48 | 49 | final int targetWidth = getWidth() - getPaddingLeft() + getPaddingRight(); 50 | AsyncTweetElementFactory.setTargetWidth(context, targetWidth); 51 | App.getInstance(context).getElementCache().evictAll(); 52 | 53 | TweetsAdapter adapter = (TweetsAdapter) getAdapter(); 54 | if (adapter != null) { 55 | adapter.notifyDataSetChanged(); 56 | } 57 | } 58 | 59 | private void updateItemLoader() { 60 | Context context = getContext(); 61 | 62 | if (mPresenterId == R.layout.tweet_async_row) { 63 | TweetsLayoutLoader loader = new TweetsLayoutLoader(context); 64 | 65 | ItemManager.Builder builder = new ItemManager.Builder(loader); 66 | builder.setPreloadItemsEnabled(true).setPreloadItemsCount(30); 67 | builder.setThreadPoolSize(1); 68 | setItemManager(builder.build()); 69 | } 70 | } 71 | 72 | @Override 73 | protected void onAttachedToWindow() { 74 | super.onAttachedToWindow(); 75 | 76 | getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { 77 | @Override 78 | @SuppressWarnings("deprecation") 79 | public void onGlobalLayout() { 80 | updateTargetWidth(); 81 | updateItemLoader(); 82 | 83 | mTweetsAdapter = new TweetsAdapter(getContext(), mPresenterId); 84 | setAdapter(mTweetsAdapter); 85 | 86 | getViewTreeObserver().removeGlobalOnLayoutListener(this); 87 | } 88 | }); 89 | } 90 | 91 | @Override 92 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 93 | super.onLayout(changed, l, t, r, b); 94 | 95 | if (changed) { 96 | updateTargetWidth(); 97 | } 98 | } 99 | 100 | public void setPresenter(int id) { 101 | if (mPresenterId == id) { 102 | return; 103 | } 104 | 105 | mPresenterId = id; 106 | if (mTweetsAdapter != null) { 107 | mTweetsAdapter.setPresenter(id); 108 | } 109 | 110 | updateItemLoader(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/async/AsyncTweetElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.async; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.Canvas; 22 | import android.graphics.Paint; 23 | import android.graphics.drawable.Drawable; 24 | import android.os.Looper; 25 | import android.text.TextUtils; 26 | import android.view.View; 27 | 28 | import com.squareup.picasso.Picasso; 29 | 30 | import org.lucasr.layoutsamples.adapter.Tweet; 31 | import org.lucasr.layoutsamples.app.R; 32 | import org.lucasr.layoutsamples.widget.TweetElement; 33 | import org.lucasr.layoutsamples.adapter.TweetPresenter; 34 | import org.lucasr.layoutsamples.canvas.UIElementHost; 35 | import org.lucasr.layoutsamples.canvas.UIElementWrapper; 36 | 37 | import java.util.EnumSet; 38 | 39 | public class AsyncTweetElement extends UIElementWrapper implements TweetPresenter { 40 | private final Paint mIndicatorPaint; 41 | private final int mIndicatorSize; 42 | 43 | public AsyncTweetElement(TweetElement element) { 44 | super(element); 45 | 46 | final Resources res = getResources(); 47 | 48 | mIndicatorPaint = new Paint(); 49 | mIndicatorSize = res.getDimensionPixelSize(R.dimen.tweet_padding); 50 | 51 | boolean onMainThread = (Looper.myLooper() == Looper.getMainLooper()); 52 | final int indicatorColor = onMainThread ? R.color.tweet_on_main_thread : 53 | R.color.tweet_off_main_thread; 54 | mIndicatorPaint.setColor(res.getColor(indicatorColor)); 55 | } 56 | 57 | @Override 58 | public void measure(int widthMeasureSpec, int heightMeasureSpec) { 59 | // Do nothing, the wrapped UIElement is already measured. 60 | } 61 | 62 | @Override 63 | public void layout(int left, int top, int right, int bottom) { 64 | // Do nothing, the wrapped UIElement is already sized and positioned. 65 | } 66 | 67 | @Override 68 | public void draw(Canvas canvas) { 69 | super.draw(canvas); 70 | canvas.drawRect(0, 0, mIndicatorSize, mIndicatorSize, mIndicatorPaint); 71 | } 72 | 73 | @Override 74 | public void requestLayout() { 75 | // Do nothing, we never change the wrapped element's layout. 76 | } 77 | 78 | @Override 79 | public void update(Tweet tweet, EnumSet flags) { 80 | TweetElement element = (TweetElement) getWrappedElement(); 81 | element.loadProfileImage(tweet, flags); 82 | 83 | final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl()); 84 | if (hasPostImage) { 85 | element.loadPostImage(tweet, flags); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/async/AsyncTweetElementFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.async; 18 | 19 | import android.content.Context; 20 | import android.view.View; 21 | 22 | import org.lucasr.layoutsamples.adapter.TweetPresenter; 23 | import org.lucasr.layoutsamples.app.App; 24 | import org.lucasr.layoutsamples.adapter.Tweet; 25 | import org.lucasr.layoutsamples.widget.TweetElement; 26 | 27 | import java.util.EnumSet; 28 | 29 | public class AsyncTweetElementFactory { 30 | private AsyncTweetElementFactory() { 31 | } 32 | 33 | private static int sTargetWidth; 34 | private static HeadlessElementHost sHeadlessHost; 35 | 36 | public synchronized static void setTargetWidth(Context context, int targetWidth) { 37 | if (sTargetWidth == targetWidth) { 38 | return; 39 | } 40 | 41 | sTargetWidth = targetWidth; 42 | } 43 | 44 | public synchronized static AsyncTweetElement create(Context context, Tweet tweet) { 45 | UIElementCache elementCache = App.getInstance(context).getElementCache(); 46 | 47 | AsyncTweetElement asyncElement = (AsyncTweetElement) elementCache.get(tweet.getId()); 48 | if (asyncElement != null) { 49 | return asyncElement; 50 | } 51 | 52 | final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(sTargetWidth, 53 | View.MeasureSpec.EXACTLY); 54 | final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, 55 | View.MeasureSpec.UNSPECIFIED); 56 | 57 | if (sHeadlessHost == null) { 58 | sHeadlessHost = new HeadlessElementHost(context); 59 | } 60 | 61 | final TweetElement element = new TweetElement(sHeadlessHost); 62 | element.update(tweet, EnumSet.of(TweetPresenter.UpdateFlags.NO_IMAGE_LOADING)); 63 | element.measure(widthMeasureSpec, heightMeasureSpec); 64 | element.layout(0, 0, element.getMeasuredWidth(), element.getMeasuredHeight()); 65 | 66 | asyncElement = new AsyncTweetElement(element); 67 | elementCache.put(tweet.getId(), asyncElement); 68 | 69 | return asyncElement; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/async/AsyncTweetView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.async; 18 | 19 | import android.content.Context; 20 | import android.graphics.drawable.Drawable; 21 | import android.util.AttributeSet; 22 | 23 | import org.lucasr.layoutsamples.app.App; 24 | import org.lucasr.layoutsamples.adapter.Tweet; 25 | import org.lucasr.layoutsamples.adapter.TweetPresenter; 26 | import org.lucasr.layoutsamples.canvas.UIElement; 27 | import org.lucasr.layoutsamples.canvas.UIElementView; 28 | 29 | import java.util.EnumSet; 30 | 31 | public class AsyncTweetView extends UIElementView implements TweetPresenter { 32 | private Tweet mTweet; 33 | 34 | public AsyncTweetView(Context context, AttributeSet attrs) { 35 | this(context, attrs, 0); 36 | } 37 | 38 | public AsyncTweetView(Context context, AttributeSet attrs, int defStyleAttr) { 39 | super(context, attrs, defStyleAttr); 40 | } 41 | 42 | @Override 43 | public void update(Tweet tweet, EnumSet flags) { 44 | mTweet = tweet; 45 | 46 | final AsyncTweetElement element = AsyncTweetElementFactory.create(getContext(), tweet); 47 | setUIElement(element); 48 | 49 | element.update(tweet, flags); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/async/HeadlessElementHost.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.async; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.drawable.Drawable; 22 | import android.view.ViewGroup.LayoutParams; 23 | 24 | import org.lucasr.layoutsamples.canvas.UIElementHost; 25 | 26 | public class HeadlessElementHost implements UIElementHost { 27 | private final Context mContext; 28 | 29 | public HeadlessElementHost(Context context) { 30 | mContext = context; 31 | } 32 | 33 | @Override 34 | public void requestLayout() { 35 | } 36 | 37 | @Override 38 | public void invalidate() { 39 | } 40 | 41 | @Override 42 | public void invalidate(int left, int top, int right, int bottom) { 43 | } 44 | 45 | @Override 46 | public int[] getDrawableState() { 47 | return new int[0]; 48 | } 49 | 50 | @Override 51 | public Context getContext() { 52 | return mContext; 53 | } 54 | 55 | @Override 56 | public Resources getResources() { 57 | return mContext.getResources(); 58 | } 59 | 60 | @Override 61 | public void invalidateDrawable(Drawable who) { 62 | } 63 | 64 | @Override 65 | public void scheduleDrawable(Drawable who, Runnable what, long when) { 66 | } 67 | 68 | @Override 69 | public void unscheduleDrawable(Drawable who) { 70 | } 71 | 72 | @Override 73 | public void unscheduleDrawable(Drawable who, Runnable what) { 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/async/TweetsLayoutLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.async; 18 | 19 | import android.content.Context; 20 | import android.view.View; 21 | import android.widget.Adapter; 22 | 23 | import org.lucasr.layoutsamples.adapter.Tweet; 24 | import org.lucasr.layoutsamples.app.App; 25 | import org.lucasr.layoutsamples.canvas.UIElement; 26 | import org.lucasr.smoothie.SimpleItemLoader; 27 | 28 | public class TweetsLayoutLoader extends SimpleItemLoader { 29 | private final Context mContext; 30 | private final UIElementCache mElementCache; 31 | 32 | public TweetsLayoutLoader(Context context) { 33 | mContext = context; 34 | mElementCache = App.getInstance(context).getElementCache(); 35 | } 36 | 37 | @Override 38 | public Tweet getItemParams(Adapter adapter, int position) { 39 | return (Tweet) adapter.getItem(position); 40 | } 41 | 42 | @Override 43 | public UIElement loadItem(Tweet tweet) { 44 | return AsyncTweetElementFactory.create(mContext, tweet); 45 | } 46 | 47 | @Override 48 | public UIElement loadItemFromMemory(Tweet tweet) { 49 | return mElementCache.get(tweet.getId()); 50 | } 51 | 52 | @Override 53 | public void displayItem(View itemView, UIElement result, boolean fromMemory) { 54 | // Do nothing as we're only using this loader to pre-measure/layout 55 | // TweetElements that are off screen. 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/async/UIElementCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.async; 18 | 19 | import android.util.LruCache; 20 | 21 | import org.lucasr.layoutsamples.canvas.UIElement; 22 | 23 | public class UIElementCache extends LruCache { 24 | private static final int MAX_CACHED_ELEMENTS = 30; 25 | 26 | public UIElementCache() { 27 | super(MAX_CACHED_ELEMENTS); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/AbstractUIElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.canvas; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.Rect; 24 | import android.util.AttributeSet; 25 | import android.view.View; 26 | import android.view.ViewGroup.LayoutParams; 27 | 28 | import org.lucasr.layoutsamples.app.R; 29 | 30 | public abstract class AbstractUIElement implements UIElement { 31 | protected UIElementHost mHost; 32 | 33 | private int mId; 34 | 35 | private int mMeasuredWidth; 36 | private int mMeasuredHeight; 37 | 38 | private Rect mBounds = new Rect(); 39 | private Rect mPadding = new Rect(); 40 | 41 | private LayoutParams mLayoutParams; 42 | 43 | private int mVisibility = View.VISIBLE; 44 | 45 | public AbstractUIElement(UIElementHost host) { 46 | this(host, null); 47 | } 48 | 49 | public AbstractUIElement(UIElementHost host, AttributeSet attrs) { 50 | swapHost(host); 51 | 52 | TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.UIElement, 0, 0); 53 | 54 | final int indexCount = a.getIndexCount(); 55 | for (int i = 0; i < indexCount; i++) { 56 | final int attr = a.getIndex(i); 57 | 58 | switch (attr) { 59 | case R.styleable.UIElement_android_padding: 60 | final int padding = a.getDimensionPixelSize(attr, 0); 61 | mPadding.left = mPadding.top = mPadding.right = mPadding.bottom = padding; 62 | break; 63 | case R.styleable.UIElement_android_paddingLeft: 64 | mPadding.left = a.getDimensionPixelSize(attr, 0); 65 | break; 66 | case R.styleable.UIElement_android_paddingTop: 67 | mPadding.top = a.getDimensionPixelSize(attr, 0); 68 | break; 69 | case R.styleable.UIElement_android_paddingRight: 70 | mPadding.right = a.getDimensionPixelSize(attr, 0); 71 | break; 72 | case R.styleable.UIElement_android_paddingBottom: 73 | mPadding.bottom = a.getDimensionPixelSize(attr, 0); 74 | break; 75 | case R.styleable.UIElement_android_id: 76 | mId = a.getResourceId(attr, -1); 77 | break; 78 | case R.styleable.UIElement_android_visibility: 79 | mVisibility = a.getInt(attr, View.VISIBLE); 80 | break; 81 | } 82 | } 83 | 84 | a.recycle(); 85 | } 86 | 87 | protected void onAttachedToHost() { 88 | } 89 | 90 | protected void onDetachedFromHost() { 91 | } 92 | 93 | @Override 94 | public boolean swapHost(UIElementHost host) { 95 | if (mHost == host) { 96 | return false; 97 | } 98 | 99 | if (mHost != null) { 100 | onDetachedFromHost(); 101 | } 102 | 103 | mHost = host; 104 | 105 | if (mHost != null) { 106 | onAttachedToHost(); 107 | } 108 | 109 | return true; 110 | } 111 | 112 | @Override 113 | public boolean isAttachedToHost() { 114 | return (mHost != null); 115 | } 116 | 117 | @Override 118 | public int getId() { 119 | return mId; 120 | } 121 | 122 | protected void setMeasuredDimension(int width, int height) { 123 | mMeasuredWidth = width; 124 | mMeasuredHeight = height; 125 | } 126 | 127 | @Override 128 | public int getMeasuredWidth() { 129 | return mMeasuredWidth; 130 | } 131 | 132 | @Override 133 | public int getMeasuredHeight() { 134 | return mMeasuredHeight; 135 | } 136 | 137 | @Override 138 | public int getPaddingLeft() { 139 | return mPadding.left; 140 | } 141 | 142 | @Override 143 | public int getPaddingTop() { 144 | return mPadding.top; 145 | } 146 | 147 | @Override 148 | public int getPaddingRight() { 149 | return mPadding.right; 150 | } 151 | 152 | @Override 153 | public int getPaddingBottom() { 154 | return mPadding.bottom; 155 | } 156 | 157 | @Override 158 | public void setPadding(int left, int top, int right, int bottom) { 159 | mPadding.left = left; 160 | mPadding.top = top; 161 | mPadding.right = right; 162 | mPadding.bottom = bottom; 163 | 164 | requestLayout(); 165 | } 166 | 167 | @Override 168 | public int getVisibility() { 169 | return mVisibility; 170 | } 171 | 172 | @Override 173 | public void setVisibility(int visibility) { 174 | if (mVisibility == visibility) { 175 | return; 176 | } 177 | 178 | mVisibility = visibility; 179 | 180 | requestLayout(); 181 | invalidate(); 182 | } 183 | 184 | @Override 185 | public final void draw(Canvas canvas) { 186 | final int saveCount = canvas.getSaveCount(); 187 | canvas.save(); 188 | 189 | canvas.clipRect(mBounds); 190 | canvas.translate(mBounds.left, mBounds.top); 191 | 192 | onDraw(canvas); 193 | 194 | canvas.restoreToCount(saveCount); 195 | } 196 | 197 | @Override 198 | public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 199 | onMeasure(widthMeasureSpec, heightMeasureSpec); 200 | } 201 | 202 | @Override 203 | public final void layout(int left, int top, int right, int bottom) { 204 | mBounds.left = left; 205 | mBounds.top = top; 206 | mBounds.right = right; 207 | mBounds.bottom = bottom; 208 | 209 | onLayout(left, top, right, bottom); 210 | } 211 | 212 | @Override 213 | public int getLeft() { 214 | return mBounds.left; 215 | } 216 | 217 | @Override 218 | public int getTop() { 219 | return mBounds.top; 220 | } 221 | 222 | @Override 223 | public int getRight() { 224 | return mBounds.right; 225 | } 226 | 227 | @Override 228 | public int getBottom() { 229 | return mBounds.bottom; 230 | } 231 | 232 | @Override 233 | public int getWidth() { 234 | return mBounds.right - mBounds.left; 235 | } 236 | 237 | @Override 238 | public int getHeight() { 239 | return mBounds.bottom - mBounds.top; 240 | } 241 | 242 | @Override 243 | public void setLayoutParams(LayoutParams lp) { 244 | if (mLayoutParams == lp) { 245 | return; 246 | } 247 | 248 | mLayoutParams = lp; 249 | requestLayout(); 250 | } 251 | 252 | @Override 253 | public LayoutParams getLayoutParams() { 254 | return mLayoutParams; 255 | } 256 | 257 | @Override 258 | public void onFinishInflate() { 259 | } 260 | 261 | @Override 262 | public Context getContext() { 263 | return mHost.getContext(); 264 | } 265 | 266 | @Override 267 | public Resources getResources() { 268 | return mHost.getResources(); 269 | } 270 | 271 | @Override 272 | public void requestLayout() { 273 | mHost.requestLayout(); 274 | } 275 | 276 | @Override 277 | public void invalidate() { 278 | mHost.invalidate(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); 279 | } 280 | 281 | protected abstract void onDraw(Canvas canvas); 282 | 283 | protected abstract void onMeasure(int widthMeasureSpec, int heightMeasureSpec); 284 | 285 | protected abstract void onLayout(int left, int top, int right, int bottom); 286 | 287 | public abstract void drawableStateChanged(); 288 | } 289 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/ImageElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.canvas; 18 | 19 | import android.content.res.Resources; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Bitmap; 22 | import android.graphics.Canvas; 23 | import android.graphics.Matrix; 24 | import android.graphics.RectF; 25 | import android.graphics.drawable.BitmapDrawable; 26 | import android.graphics.drawable.Drawable; 27 | import android.util.AttributeSet; 28 | import android.util.Log; 29 | import android.view.View; 30 | import android.view.View.MeasureSpec; 31 | import android.widget.ImageView.ScaleType; 32 | 33 | import org.lucasr.layoutsamples.app.R; 34 | 35 | public class ImageElement extends AbstractUIElement implements Drawable.Callback { 36 | private static final String LOGTAG = "ImageElement"; 37 | 38 | private static final ScaleType[] sScaleTypeArray = { 39 | ScaleType.MATRIX, 40 | ScaleType.FIT_XY, 41 | ScaleType.FIT_START, 42 | ScaleType.FIT_CENTER, 43 | ScaleType.FIT_END, 44 | ScaleType.CENTER, 45 | ScaleType.CENTER_CROP, 46 | ScaleType.CENTER_INSIDE 47 | }; 48 | 49 | private Drawable mDrawable; 50 | private int mResourceId; 51 | 52 | private int mDrawableWidth; 53 | private int mDrawableHeight; 54 | 55 | private final Matrix mMatrix; 56 | private Matrix mDrawMatrix; 57 | 58 | private RectF mTempSrc = new RectF(); 59 | private RectF mTempDst = new RectF(); 60 | 61 | private ScaleType mScaleType; 62 | private int mLevel; 63 | 64 | public ImageElement(UIElementHost host) { 65 | this(host, null); 66 | } 67 | 68 | public ImageElement(UIElementHost host, AttributeSet attrs) { 69 | super(host, attrs); 70 | mMatrix = new Matrix(); 71 | mScaleType = ScaleType.FIT_CENTER; 72 | 73 | TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ImageElement, 0, 0); 74 | 75 | final int indexCount = a.getIndexCount(); 76 | for (int i = 0; i < indexCount; i++) { 77 | final int attr = a.getIndex(i); 78 | 79 | switch (attr) { 80 | case R.styleable.ImageElement_android_src: 81 | final Drawable d = a.getDrawable(attr); 82 | if (d != null) { 83 | setImageDrawable(d); 84 | } 85 | break; 86 | 87 | case R.styleable.ImageElement_android_scaleType: 88 | final int index = a.getInt(attr, -1); 89 | if (index >= 0) { 90 | setScaleType(sScaleTypeArray[index]); 91 | } 92 | break; 93 | } 94 | } 95 | 96 | a.recycle(); 97 | } 98 | 99 | private void configureBounds() { 100 | if (mDrawable == null) { 101 | return; 102 | } 103 | 104 | final int dwidth = mDrawableWidth; 105 | final int dheight = mDrawableHeight; 106 | 107 | final int vwidth = getWidth() - getPaddingLeft() - getPaddingRight(); 108 | final int vheight = getHeight() - getPaddingTop() - getPaddingBottom(); 109 | 110 | boolean fits = (dwidth < 0 || vwidth == dwidth) && 111 | (dheight < 0 || vheight == dheight); 112 | 113 | if (dwidth <= 0 || dheight <= 0 || mScaleType == ScaleType.FIT_XY) { 114 | // If the drawable has no intrinsic size, or we're told to 115 | // scaletofit, then we just fill our entire view. 116 | mDrawable.setBounds(0, 0, vwidth, vheight); 117 | mDrawMatrix = null; 118 | } else { 119 | // We need to do the scaling ourself, so have the drawable 120 | // use its native size. 121 | mDrawable.setBounds(0, 0, dwidth, dheight); 122 | 123 | if (mScaleType == ScaleType.MATRIX) { 124 | // Use the specified matrix as-is. 125 | if (mMatrix.isIdentity()) { 126 | mDrawMatrix = null; 127 | } else { 128 | mDrawMatrix = mMatrix; 129 | } 130 | } else if (fits) { 131 | // The bitmap fits exactly, no transform needed. 132 | mDrawMatrix = null; 133 | } else if (mScaleType == ScaleType.CENTER) { 134 | // Center bitmap in view, no scaling. 135 | mDrawMatrix = mMatrix; 136 | mDrawMatrix.setTranslate((int) ((vwidth - dwidth) * 0.5f + 0.5f), 137 | (int) ((vheight - dheight) * 0.5f + 0.5f)); 138 | } else if (mScaleType == ScaleType.CENTER_CROP) { 139 | mDrawMatrix = mMatrix; 140 | 141 | float scale; 142 | float dx = 0, dy = 0; 143 | 144 | if (dwidth * vheight > vwidth * dheight) { 145 | scale = (float) vheight / (float) dheight; 146 | dx = (vwidth - dwidth * scale) * 0.5f; 147 | } else { 148 | scale = (float) vwidth / (float) dwidth; 149 | dy = (vheight - dheight * scale) * 0.5f; 150 | } 151 | 152 | mDrawMatrix.setScale(scale, scale); 153 | mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); 154 | } else if (mScaleType == ScaleType.CENTER_INSIDE) { 155 | mDrawMatrix = mMatrix; 156 | 157 | final float scale; 158 | if (dwidth <= vwidth && dheight <= vheight) { 159 | scale = 1.0f; 160 | } else { 161 | scale = Math.min((float) vwidth / (float) dwidth, 162 | (float) vheight / (float) dheight); 163 | } 164 | 165 | float dx = (int) ((vwidth - dwidth * scale) * 0.5f + 0.5f); 166 | float dy = (int) ((vheight - dheight * scale) * 0.5f + 0.5f); 167 | 168 | mDrawMatrix.setScale(scale, scale); 169 | mDrawMatrix.postTranslate(dx, dy); 170 | } else { 171 | // Generate the required transform. 172 | mTempSrc.set(0, 0, dwidth, dheight); 173 | mTempDst.set(0, 0, vwidth, vheight); 174 | 175 | mDrawMatrix = mMatrix; 176 | mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType)); 177 | } 178 | } 179 | } 180 | 181 | private static final Matrix.ScaleToFit[] sS2FArray = { 182 | Matrix.ScaleToFit.FILL, 183 | Matrix.ScaleToFit.START, 184 | Matrix.ScaleToFit.CENTER, 185 | Matrix.ScaleToFit.END 186 | }; 187 | 188 | private static Matrix.ScaleToFit scaleTypeToScaleToFit(ScaleType st) { 189 | // ScaleToFit enum to their corresponding Matrix.ScaleToFit values 190 | return sS2FArray[st.ordinal() - 1]; 191 | } 192 | 193 | private void updateDrawable(Drawable d) { 194 | if (mDrawable != null) { 195 | mDrawable.setCallback(null); 196 | mHost.unscheduleDrawable(mDrawable); 197 | } 198 | 199 | mDrawable = d; 200 | 201 | if (d != null) { 202 | d.setCallback(this); 203 | 204 | if (d.isStateful()) { 205 | d.setState(mHost.getDrawableState()); 206 | } 207 | 208 | d.setLevel(mLevel); 209 | d.setVisible(getVisibility() == View.VISIBLE, true); 210 | 211 | mDrawableWidth = d.getIntrinsicWidth(); 212 | mDrawableHeight = d.getIntrinsicHeight(); 213 | 214 | configureBounds(); 215 | } else { 216 | mDrawableWidth = mDrawableHeight = -1; 217 | } 218 | } 219 | 220 | private void resizeFromDrawable() { 221 | if (mDrawable == null) { 222 | return; 223 | } 224 | 225 | int width = mDrawable.getIntrinsicWidth(); 226 | if (width < 0) { 227 | width = mDrawableWidth; 228 | } 229 | 230 | int height = mDrawable.getIntrinsicHeight(); 231 | if (height < 0) { 232 | height = mDrawableHeight; 233 | } 234 | 235 | if (width != mDrawableWidth || height != mDrawableHeight) { 236 | mDrawableWidth = width; 237 | mDrawableHeight = height; 238 | 239 | requestLayout(); 240 | } 241 | } 242 | 243 | private void resolveUri() { 244 | if (mDrawable != null) { 245 | return; 246 | } 247 | 248 | Drawable d = null; 249 | 250 | if (mResourceId != 0) { 251 | try { 252 | final Resources res = getResources(); 253 | if (res == null) { 254 | return; 255 | } 256 | 257 | d = res.getDrawable(mResourceId); 258 | } catch (Exception e) { 259 | Log.w(LOGTAG, "Unable to find resource: " + mResourceId, e); 260 | } 261 | } else { 262 | return; 263 | } 264 | 265 | updateDrawable(d); 266 | } 267 | 268 | private void setDrawableVisible(boolean visible) { 269 | if (mDrawable != null) { 270 | mDrawable.setVisible(visible, false); 271 | } 272 | } 273 | 274 | @Override 275 | protected void onDraw(Canvas canvas) { 276 | if (mDrawable == null) { 277 | return; 278 | } 279 | 280 | if (mDrawableWidth == 0 || mDrawableHeight == 0) { 281 | return; 282 | } 283 | 284 | final int paddingLeft = getPaddingLeft(); 285 | final int paddingTop = getPaddingTop(); 286 | 287 | if (mDrawMatrix == null && paddingLeft == 0 && paddingTop == 0) { 288 | mDrawable.draw(canvas); 289 | } else { 290 | final int saveCount = canvas.getSaveCount(); 291 | canvas.save(); 292 | 293 | canvas.translate(paddingLeft, paddingTop); 294 | 295 | if (mDrawMatrix != null) { 296 | canvas.concat(mDrawMatrix); 297 | } 298 | mDrawable.draw(canvas); 299 | 300 | canvas.restoreToCount(saveCount); 301 | } 302 | } 303 | 304 | @Override 305 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 306 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 307 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 308 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 309 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 310 | 311 | resolveUri(); 312 | 313 | int width; 314 | int height; 315 | 316 | if (widthMode == MeasureSpec.EXACTLY) { 317 | width = widthSize; 318 | } else { 319 | if (mDrawable == null) { 320 | width = 0; 321 | mDrawableWidth = -1; 322 | } else { 323 | width = Math.max(1, mDrawableWidth) + 324 | getPaddingLeft() + getPaddingRight(); 325 | } 326 | 327 | if (widthMode == MeasureSpec.AT_MOST) { 328 | width = Math.min(widthSize, width); 329 | } 330 | } 331 | 332 | if (heightMode == MeasureSpec.EXACTLY) { 333 | height = heightSize; 334 | } else { 335 | if (mDrawable == null) { 336 | height = 0; 337 | mDrawableHeight = -1; 338 | } else { 339 | height = Math.max(1, mDrawableHeight) + 340 | getPaddingTop() + getPaddingBottom(); 341 | } 342 | 343 | if (heightMode == MeasureSpec.AT_MOST) { 344 | height = Math.min(heightSize, height); 345 | } 346 | } 347 | 348 | setMeasuredDimension(width, height); 349 | } 350 | 351 | @Override 352 | protected void onLayout(int left, int top, int right, int bottom) { 353 | configureBounds(); 354 | } 355 | 356 | @Override 357 | public void drawableStateChanged() { 358 | if (isAttachedToHost() && mDrawable != null && mDrawable.isStateful()) { 359 | mDrawable.setState(mHost.getDrawableState()); 360 | } 361 | } 362 | 363 | @Override 364 | public void setVisibility(int visibility) { 365 | super.setVisibility(visibility); 366 | setDrawableVisible(visibility == View.VISIBLE); 367 | } 368 | 369 | @Override 370 | public void invalidateDrawable(Drawable who) { 371 | if (!isAttachedToHost()) { 372 | return; 373 | } 374 | 375 | if (mDrawable == who) { 376 | mHost.invalidate(); 377 | } else { 378 | mHost.invalidateDrawable(who); 379 | } 380 | } 381 | 382 | @Override 383 | public void scheduleDrawable(Drawable who, Runnable what, long when) { 384 | if (isAttachedToHost()) { 385 | mHost.scheduleDrawable(who, what, when); 386 | } 387 | } 388 | 389 | @Override 390 | public void unscheduleDrawable(Drawable who, Runnable what) { 391 | if (isAttachedToHost()) { 392 | mHost.unscheduleDrawable(who, what); 393 | } 394 | } 395 | 396 | @Override 397 | public void onAttachedToHost() { 398 | super.onAttachedToHost(); 399 | setDrawableVisible(getVisibility() == View.VISIBLE); 400 | } 401 | 402 | @Override 403 | public void onDetachedFromHost() { 404 | super.onDetachedFromHost(); 405 | setDrawableVisible(false); 406 | } 407 | 408 | public void setImageLevel(int level) { 409 | mLevel = level; 410 | 411 | if (mDrawable != null) { 412 | mDrawable.setLevel(level); 413 | resizeFromDrawable(); 414 | } 415 | } 416 | 417 | public void setImageResource(int resourceId) { 418 | if (mResourceId == resourceId) { 419 | return; 420 | } 421 | 422 | updateDrawable(null); 423 | mResourceId = resourceId; 424 | 425 | final int oldWidth = mDrawableWidth; 426 | final int oldHeight = mDrawableHeight; 427 | 428 | resolveUri(); 429 | 430 | if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { 431 | requestLayout(); 432 | } 433 | invalidate(); 434 | } 435 | 436 | public void setImageDrawable(Drawable drawable) { 437 | if (mDrawable == drawable) { 438 | return; 439 | } 440 | 441 | mResourceId = 0; 442 | 443 | final int oldWidth = mDrawableWidth; 444 | final int oldHeight = mDrawableHeight; 445 | 446 | updateDrawable(drawable); 447 | 448 | if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) { 449 | requestLayout(); 450 | } 451 | invalidate(); 452 | } 453 | 454 | public void setImageBitmap(Bitmap bitmap) { 455 | setImageDrawable(new BitmapDrawable(getResources(), bitmap)); 456 | } 457 | 458 | public void setScaleType(ScaleType scaleType) { 459 | if (scaleType == null) { 460 | throw new NullPointerException(); 461 | } 462 | 463 | if (mScaleType == scaleType) { 464 | return; 465 | } 466 | 467 | mScaleType = scaleType; 468 | 469 | requestLayout(); 470 | invalidate(); 471 | } 472 | } -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/StaticLayoutWithMaxLines.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.canvas; 18 | 19 | import android.os.Build; 20 | import android.text.Layout.Alignment; 21 | import android.text.StaticLayout; 22 | import android.text.TextDirectionHeuristic; 23 | import android.text.TextDirectionHeuristics; 24 | import android.text.TextPaint; 25 | import android.text.TextUtils.TruncateAt; 26 | import android.util.Log; 27 | 28 | import java.lang.reflect.Constructor; 29 | 30 | public class StaticLayoutWithMaxLines { 31 | private static final String LOGTAG = "StaticLayoutWithMaxLines"; 32 | 33 | private static final String TEXT_DIR_CLASS = "android.text.TextDirectionHeuristic"; 34 | private static final String TEXT_DIRS_CLASS = "android.text.TextDirectionHeuristics"; 35 | private static final String TEXT_DIR_FIRSTSTRONG_LTR = "FIRSTSTRONG_LTR"; 36 | 37 | private static boolean sInitialized; 38 | 39 | private static Constructor sConstructor; 40 | private static Object[] sConstructorArgs; 41 | private static Object sTextDirection; 42 | 43 | public static synchronized void ensureInitialized() { 44 | if (sInitialized) { 45 | return; 46 | } 47 | 48 | try { 49 | final Class textDirClass; 50 | if (Build.VERSION.SDK_INT >= 18) { 51 | textDirClass = TextDirectionHeuristic.class; 52 | sTextDirection = TextDirectionHeuristics.FIRSTSTRONG_LTR; 53 | } else { 54 | final ClassLoader loader = StaticLayoutWithMaxLines.class.getClassLoader(); 55 | textDirClass = loader.loadClass(TEXT_DIR_CLASS); 56 | 57 | final Class textDirsClass = loader.loadClass(TEXT_DIRS_CLASS); 58 | sTextDirection = textDirsClass.getField(TEXT_DIR_FIRSTSTRONG_LTR) 59 | .get(textDirsClass); 60 | } 61 | 62 | final Class[] signature = new Class[] { 63 | CharSequence.class, 64 | int.class, 65 | int.class, 66 | TextPaint.class, 67 | int.class, 68 | Alignment.class, 69 | textDirClass, 70 | float.class, 71 | float.class, 72 | boolean.class, 73 | TruncateAt.class, 74 | int.class, 75 | int.class 76 | }; 77 | 78 | // Make the StaticLayout constructor with max lines public 79 | sConstructor = StaticLayout.class.getDeclaredConstructor(signature); 80 | sConstructor.setAccessible(true); 81 | sConstructorArgs = new Object[signature.length]; 82 | } catch (NoSuchMethodException e) { 83 | Log.e(LOGTAG, "StaticLayout constructor with max lines not found.", e); 84 | } catch (ClassNotFoundException e) { 85 | Log.e(LOGTAG, "TextDirectionHeuristic class not found.", e); 86 | } catch (NoSuchFieldException e) { 87 | Log.e(LOGTAG, "TextDirectionHeuristics.FIRSTSTRONG_LTR not found.", e); 88 | } catch (IllegalAccessException e) { 89 | Log.e(LOGTAG, "TextDirectionHeuristics.FIRSTSTRONG_LTR not accessible.", e); 90 | } finally { 91 | sInitialized = true; 92 | } 93 | } 94 | 95 | public static boolean isSupported() { 96 | if (Build.VERSION.SDK_INT < 14) { 97 | return false; 98 | } 99 | 100 | ensureInitialized(); 101 | return (sConstructor != null); 102 | } 103 | 104 | public static synchronized StaticLayout create(CharSequence source, int bufstart, int bufend, 105 | TextPaint paint, int outerWidth, Alignment align, 106 | float spacingMult, float spacingAdd, 107 | boolean includePad, TruncateAt ellipsize, 108 | int ellipsisWidth, int maxLines) { 109 | ensureInitialized(); 110 | 111 | try { 112 | sConstructorArgs[0] = source; 113 | sConstructorArgs[1] = bufstart; 114 | sConstructorArgs[2] = bufend; 115 | sConstructorArgs[3] = paint; 116 | sConstructorArgs[4] = outerWidth; 117 | sConstructorArgs[5] = align; 118 | sConstructorArgs[6] = sTextDirection; 119 | sConstructorArgs[7] = spacingMult; 120 | sConstructorArgs[8] = spacingAdd; 121 | sConstructorArgs[9] = includePad; 122 | sConstructorArgs[10] = ellipsize; 123 | sConstructorArgs[11] = ellipsisWidth; 124 | sConstructorArgs[12] = maxLines; 125 | 126 | return sConstructor.newInstance(sConstructorArgs); 127 | } catch (Exception e) { 128 | throw new IllegalStateException("Error creating StaticLayout with max lines: " + e); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/TextElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.canvas; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.content.res.Resources; 21 | import android.content.res.TypedArray; 22 | import android.graphics.Canvas; 23 | import android.graphics.Paint; 24 | import android.text.BoringLayout; 25 | import android.text.Layout; 26 | import android.text.StaticLayout; 27 | import android.text.TextPaint; 28 | import android.text.TextUtils; 29 | import android.text.TextUtils.TruncateAt; 30 | import android.util.AttributeSet; 31 | import android.util.FloatMath; 32 | import android.util.Log; 33 | import android.util.TypedValue; 34 | import android.view.View.MeasureSpec; 35 | import android.view.ViewGroup; 36 | import android.view.ViewGroup.LayoutParams; 37 | 38 | import org.lucasr.layoutsamples.app.R; 39 | 40 | public class TextElement extends AbstractUIElement { 41 | private static final String LOGTAG = "TextElement"; 42 | 43 | private CharSequence mText; 44 | 45 | private ColorStateList mTextColor; 46 | private int mCurTextColor; 47 | 48 | private int mMaxLines = Integer.MAX_VALUE; 49 | private int mOldMaxLines = Integer.MAX_VALUE; 50 | 51 | private float mLineSpacingMult = 1.0f; 52 | private float mLineSpacingAdd = 0.0f; 53 | private boolean mIncludeFontPadding = true; 54 | private Layout.Alignment mLayoutAlignment = Layout.Alignment.ALIGN_NORMAL; 55 | 56 | private Layout mLayout; 57 | private BoringLayout mSavedLayout; 58 | 59 | private final TextPaint mPaint; 60 | private TextUtils.TruncateAt mEllipsize; 61 | private BoringLayout.Metrics mBoring; 62 | 63 | private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics(); 64 | 65 | public TextElement(UIElementHost host) { 66 | this(host, null); 67 | } 68 | 69 | public TextElement(UIElementHost host, AttributeSet attrs) { 70 | super(host, attrs); 71 | 72 | mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 73 | mPaint.density = getResources().getDisplayMetrics().density; 74 | 75 | setTextColor(ColorStateList.valueOf(0xFF000000)); 76 | 77 | TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.TextElement, 0, 0); 78 | 79 | final int indexCount = a.getIndexCount(); 80 | for (int i = 0; i < indexCount; i++) { 81 | final int attr = a.getIndex(i); 82 | 83 | switch (attr) { 84 | case R.styleable.TextElement_android_textSize: 85 | final int textSize = a.getDimensionPixelSize(attr, -1); 86 | if (textSize >= 0) { 87 | setRawTextSize(textSize); 88 | } 89 | break; 90 | 91 | case R.styleable.TextElement_android_textColor: 92 | final ColorStateList textColors = a.getColorStateList(attr); 93 | if (textColors != null) { 94 | setTextColor(textColors); 95 | } 96 | break; 97 | 98 | case R.styleable.TextElement_android_maxLines: 99 | final int maxLines = a.getInt(attr, -1); 100 | if (maxLines > 0) { 101 | setMaxLines(maxLines); 102 | } 103 | break; 104 | 105 | case R.styleable.TextElement_android_ellipsize: 106 | final int ellipsize = a.getInt(attr, -1); 107 | switch (ellipsize) { 108 | case 1: 109 | setEllipsize(TextUtils.TruncateAt.START); 110 | break; 111 | case 2: 112 | setEllipsize(TextUtils.TruncateAt.MIDDLE); 113 | break; 114 | case 3: 115 | setEllipsize(TextUtils.TruncateAt.END); 116 | break; 117 | case 4: 118 | Log.w(LOGTAG, "Marquee ellipsize is not supported"); 119 | break; 120 | } 121 | break; 122 | } 123 | } 124 | 125 | a.recycle(); 126 | } 127 | 128 | private int getDesiredWidth() { 129 | final int lineCount = mLayout.getLineCount(); 130 | final CharSequence text = mLayout.getText(); 131 | 132 | // If any line was wrapped, we can't use it. but it's 133 | // ok for the last line not to have a newline. 134 | for (int i = 0; i < lineCount - 1; i++) { 135 | if (text.charAt(mLayout.getLineEnd(i) - 1) != '\n') { 136 | return -1; 137 | } 138 | } 139 | 140 | float maxWidth = 0; 141 | for (int i = 0; i < lineCount; i++) { 142 | maxWidth = Math.max(maxWidth, mLayout.getLineWidth(i)); 143 | } 144 | 145 | return (int) FloatMath.ceil(maxWidth); 146 | } 147 | 148 | private int getDesiredHeight() { 149 | if (mLayout == null) { 150 | return 0; 151 | } 152 | 153 | final int padding = getPaddingTop() + getPaddingBottom(); 154 | final int refLine = Math.min(mMaxLines, mLayout.getLineCount()); 155 | 156 | return mLayout.getLineTop(refLine) + padding; 157 | } 158 | 159 | private void makeNewLayout(int wantWidth, BoringLayout.Metrics boring, 160 | int ellipsisWidth, boolean bringIntoView) { 161 | if (wantWidth < 0) { 162 | wantWidth = 0; 163 | } 164 | 165 | mOldMaxLines = mMaxLines; 166 | boolean shouldEllipsize = (mEllipsize != null); 167 | 168 | mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, mLayoutAlignment, 169 | shouldEllipsize, mEllipsize, bringIntoView); 170 | } 171 | 172 | private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, 173 | Layout.Alignment alignment, boolean shouldEllipsize, 174 | TruncateAt effectiveEllipsize, boolean useSaved) { 175 | Layout result; 176 | 177 | if (boring == UNKNOWN_BORING) { 178 | boring = BoringLayout.isBoring(mText, mPaint, mBoring); 179 | if (boring != null) { 180 | mBoring = boring; 181 | } 182 | } 183 | 184 | if (boring != null) { 185 | // Layout is smaller than target width, no ellipsize defined. 186 | if (boring.width <= wantWidth && 187 | (effectiveEllipsize == null || boring.width <= ellipsisWidth)) { 188 | if (mSavedLayout != null) { 189 | result = mSavedLayout.replaceOrMake(mText, mPaint, wantWidth, alignment, 190 | mLineSpacingMult, mLineSpacingAdd, boring, mIncludeFontPadding); 191 | } else { 192 | result = BoringLayout.make(mText, mPaint, wantWidth, alignment, 193 | mLineSpacingMult, mLineSpacingAdd, boring, mIncludeFontPadding); 194 | } 195 | 196 | if (useSaved) { 197 | mSavedLayout = (BoringLayout) result; 198 | } 199 | 200 | // Layout is smaller than target width, ellipsize is not necessary. 201 | } else if (shouldEllipsize && boring.width <= wantWidth) { 202 | if (useSaved && mSavedLayout != null) { 203 | result = mSavedLayout.replaceOrMake(mText, mPaint, wantWidth, alignment, 204 | mLineSpacingMult, mLineSpacingAdd, boring, mIncludeFontPadding, 205 | effectiveEllipsize, ellipsisWidth); 206 | } else { 207 | result = BoringLayout.make(mText, mPaint, wantWidth, alignment, 208 | mLineSpacingMult, mLineSpacingAdd, boring, mIncludeFontPadding, 209 | effectiveEllipsize, ellipsisWidth); 210 | } 211 | 212 | // Should ellipsize, layout is bigger than target width. 213 | } else if (shouldEllipsize) { 214 | result = StaticLayoutWithMaxLines.create(mText, 0, mText.length(), mPaint, wantWidth, 215 | alignment, mLineSpacingMult, mLineSpacingAdd, mIncludeFontPadding, 216 | effectiveEllipsize, ellipsisWidth, mMaxLines); 217 | 218 | // No ellipsize, just truncate text. 219 | } else { 220 | result = new StaticLayout(mText, mPaint, wantWidth, alignment, mLineSpacingMult, 221 | mLineSpacingAdd, mIncludeFontPadding); 222 | } 223 | 224 | // Layout is not Boring and should ellipsize. 225 | } else if (shouldEllipsize) { 226 | result = StaticLayoutWithMaxLines.create(mText, 0, mText.length(), 227 | mPaint, wantWidth, alignment, mLineSpacingMult, 228 | mLineSpacingAdd, mIncludeFontPadding, effectiveEllipsize, 229 | ellipsisWidth, mMaxLines); 230 | 231 | // Layout is not boring and should not ellipsize 232 | } else { 233 | result = new StaticLayout(mText, mPaint, wantWidth, alignment, mLineSpacingMult, 234 | mLineSpacingAdd, mIncludeFontPadding); 235 | } 236 | 237 | return result; 238 | } 239 | 240 | private void resetAndSaveLayout() { 241 | if (mLayout instanceof BoringLayout && mSavedLayout == null) { 242 | mSavedLayout = (BoringLayout) mLayout; 243 | } 244 | 245 | mBoring = null; 246 | } 247 | 248 | private void checkForRelayout() { 249 | if (mLayout == null) { 250 | return; 251 | } 252 | 253 | final LayoutParams lp = getLayoutParams(); 254 | 255 | // If we have a fixed width, we can just swap in a new text layout 256 | // if the text height stays the same or if the view height is fixed. 257 | if (lp.width != LayoutParams.WRAP_CONTENT) { 258 | // Static width, so try making a new text layout. 259 | 260 | final int oldHeight = mLayout.getHeight(); 261 | final int oldWidth = mLayout.getWidth(); 262 | 263 | // No need to bring the text into view, since the size is not 264 | // changing (unless we do the requestLayout(), in which case it 265 | // will happen when measuring). 266 | makeNewLayout(oldWidth, UNKNOWN_BORING, oldWidth, false); 267 | 268 | // In a fixed-height view, so use our new text layout. 269 | if (lp.height != ViewGroup.LayoutParams.WRAP_CONTENT && 270 | lp.height != ViewGroup.LayoutParams.MATCH_PARENT) { 271 | invalidate(); 272 | return; 273 | } 274 | 275 | // Dynamic height, but height has stayed the same, 276 | // so use our new text layout. 277 | if (mLayout.getHeight() == oldHeight) { 278 | invalidate(); 279 | return; 280 | } 281 | 282 | // We lose: the height has changed and we have a dynamic height. 283 | // Request a new view layout using our new text layout. 284 | requestLayout(); 285 | invalidate(); 286 | } else { 287 | // Dynamic width, so we have no choice but to request a new 288 | // view layout with a new text layout. 289 | recreateLayout(); 290 | } 291 | } 292 | 293 | private void recreateLayout() { 294 | if (mLayout == null) { 295 | return; 296 | } 297 | 298 | resetAndSaveLayout(); 299 | requestLayout(); 300 | invalidate(); 301 | } 302 | 303 | private void updateTextColors() { 304 | int color = mTextColor.getColorForState(mHost.getDrawableState(), 0); 305 | if (color != mCurTextColor) { 306 | mCurTextColor = color; 307 | invalidate(); 308 | } 309 | } 310 | 311 | @Override 312 | protected void onDraw(Canvas canvas) { 313 | if (mLayout == null) { 314 | return; 315 | } 316 | 317 | final int saveCount = canvas.getSaveCount(); 318 | canvas.save(); 319 | 320 | mPaint.setColor(mCurTextColor); 321 | 322 | float clipLeft = getPaddingLeft(); 323 | float clipTop = getPaddingTop(); 324 | float clipRight = getRight() - getPaddingRight(); 325 | float clipBottom = getBottom() - getPaddingBottom(); 326 | canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom); 327 | 328 | canvas.translate(getPaddingLeft(), getPaddingTop()); 329 | mLayout.draw(canvas); 330 | 331 | canvas.restoreToCount(saveCount); 332 | } 333 | 334 | @Override 335 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 336 | final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 337 | final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 338 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 339 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 340 | 341 | final int paddingLeft = getPaddingLeft(); 342 | final int paddingRight = getPaddingRight(); 343 | 344 | int width; 345 | int height; 346 | 347 | BoringLayout.Metrics boring = UNKNOWN_BORING; 348 | 349 | int desiredWidth = -1; 350 | boolean fromExisting = false; 351 | 352 | if (widthMode == MeasureSpec.EXACTLY) { 353 | width = widthSize; 354 | } else { 355 | if (mLayout != null && mEllipsize == null) { 356 | desiredWidth = getDesiredWidth(); 357 | } 358 | 359 | if (desiredWidth < 0) { 360 | boring = BoringLayout.isBoring(mText, mPaint, mBoring); 361 | if (boring != null) { 362 | mBoring = boring; 363 | } 364 | } else { 365 | fromExisting = true; 366 | } 367 | 368 | if (boring == null || boring == UNKNOWN_BORING) { 369 | if (desiredWidth < 0) { 370 | desiredWidth = (int) FloatMath.ceil(Layout.getDesiredWidth(mText, mPaint)); 371 | } 372 | 373 | width = desiredWidth; 374 | } else { 375 | width = boring.width; 376 | } 377 | 378 | width += paddingLeft + paddingRight; 379 | 380 | if (widthMode == MeasureSpec.AT_MOST) { 381 | width = Math.min(widthSize, width); 382 | } 383 | } 384 | 385 | int unpaddedWidth = width - paddingLeft - paddingRight; 386 | 387 | if (mLayout == null) { 388 | makeNewLayout(unpaddedWidth, boring, unpaddedWidth, false); 389 | } else { 390 | final boolean layoutChanged = (mLayout.getWidth() != unpaddedWidth) || 391 | (mLayout.getEllipsizedWidth() != unpaddedWidth); 392 | 393 | final boolean widthChanged = 394 | (mEllipsize == null) && 395 | (unpaddedWidth > mLayout.getWidth()) && 396 | (mLayout instanceof BoringLayout || 397 | (fromExisting && desiredWidth >= 0 && desiredWidth <= unpaddedWidth)); 398 | 399 | final boolean maxChanged = (mMaxLines != mOldMaxLines); 400 | 401 | if (layoutChanged || maxChanged) { 402 | if (!maxChanged && widthChanged) { 403 | mLayout.increaseWidthTo(unpaddedWidth); 404 | } else { 405 | makeNewLayout(unpaddedWidth, boring, unpaddedWidth, false); 406 | } 407 | } 408 | } 409 | 410 | if (heightMode == MeasureSpec.EXACTLY) { 411 | height = heightSize; 412 | } else { 413 | height = getDesiredHeight(); 414 | 415 | if (heightMode == MeasureSpec.AT_MOST) { 416 | height = Math.min(heightSize, height); 417 | } 418 | } 419 | 420 | setMeasuredDimension(width, height); 421 | } 422 | 423 | @Override 424 | protected void onLayout(int left, int top, int right, int bottom) { 425 | // Do nothing. 426 | } 427 | 428 | @Override 429 | public void drawableStateChanged() { 430 | if (mTextColor != null && mTextColor.isStateful()) { 431 | updateTextColors(); 432 | } 433 | } 434 | 435 | public void setRawTextSize(float size) { 436 | if (mPaint.getTextSize() == size) { 437 | return; 438 | } 439 | 440 | mPaint.setTextSize(size); 441 | recreateLayout(); 442 | } 443 | 444 | public void setTextSize(float size) { 445 | setTextSize(TypedValue.COMPLEX_UNIT_SP, size); 446 | } 447 | 448 | public void setTextSize(int unit, float size) { 449 | final Resources res = getResources(); 450 | 451 | final float textSize = TypedValue.applyDimension(unit, size, res.getDisplayMetrics()); 452 | if (mPaint.getTextSize() == textSize) { 453 | return; 454 | } 455 | 456 | mPaint.setTextSize(textSize); 457 | recreateLayout(); 458 | } 459 | 460 | public void setTextAlignment(Layout.Alignment alignment) { 461 | if (mLayoutAlignment == alignment) { 462 | return; 463 | } 464 | 465 | mLayoutAlignment = alignment; 466 | recreateLayout(); 467 | } 468 | 469 | public void setTextColor(int color) { 470 | mTextColor = ColorStateList.valueOf(color); 471 | updateTextColors(); 472 | } 473 | 474 | public void setTextColor(ColorStateList colors) { 475 | if (colors == null) { 476 | throw new NullPointerException(); 477 | } 478 | 479 | mTextColor = colors; 480 | updateTextColors(); 481 | } 482 | 483 | public void setEllipsize(TruncateAt ellipsize) { 484 | if (mEllipsize == ellipsize) { 485 | return; 486 | } 487 | 488 | mEllipsize = ellipsize; 489 | recreateLayout(); 490 | } 491 | 492 | public void setMaxLines(int maxLines) { 493 | if (mMaxLines == maxLines) { 494 | return; 495 | } 496 | 497 | mMaxLines = maxLines; 498 | 499 | requestLayout(); 500 | invalidate(); 501 | } 502 | 503 | public int getMaxLines() { 504 | return mMaxLines; 505 | } 506 | 507 | @Override 508 | public void setPadding(int left, int top, int right, int bottom) { 509 | super.setPadding(left, top, right, bottom); 510 | recreateLayout(); 511 | } 512 | 513 | public void setText(CharSequence text) { 514 | if (TextUtils.equals(mText, text)) { 515 | return; 516 | } 517 | 518 | mText = text; 519 | checkForRelayout(); 520 | } 521 | } -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/UIElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.canvas; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.Canvas; 22 | import android.view.ViewGroup.LayoutParams; 23 | 24 | public interface UIElement { 25 | public boolean swapHost(UIElementHost host); 26 | public boolean isAttachedToHost(); 27 | 28 | public int getId(); 29 | 30 | public int getMeasuredWidth(); 31 | public int getMeasuredHeight(); 32 | 33 | public int getPaddingLeft(); 34 | public int getPaddingTop(); 35 | public int getPaddingRight(); 36 | public int getPaddingBottom(); 37 | public void setPadding(int left, int top, int right, int bottom); 38 | 39 | public int getLeft(); 40 | public int getTop(); 41 | public int getRight(); 42 | public int getBottom(); 43 | 44 | public int getWidth(); 45 | public int getHeight(); 46 | 47 | public void setLayoutParams(LayoutParams lp); 48 | public LayoutParams getLayoutParams(); 49 | 50 | public void onFinishInflate(); 51 | 52 | public void measure(int widthMeasureSpec, int heightMeasureSpec); 53 | public void layout(int left, int top, int right, int bottom); 54 | public void draw(Canvas canvas); 55 | public void drawableStateChanged(); 56 | 57 | public Context getContext(); 58 | public Resources getResources(); 59 | 60 | public void requestLayout(); 61 | public void invalidate(); 62 | 63 | public int getVisibility(); 64 | public void setVisibility(int visibility); 65 | } -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/UIElementGroup.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * Copyright (C) 2014 Lucas Rocha 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package org.lucasr.layoutsamples.canvas; 19 | 20 | import android.graphics.Canvas; 21 | import android.util.AttributeSet; 22 | import android.view.View; 23 | import android.view.View.MeasureSpec; 24 | import android.view.ViewGroup.LayoutParams; 25 | import android.view.ViewGroup.MarginLayoutParams; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | public abstract class UIElementGroup extends AbstractUIElement { 31 | private final List mElements; 32 | 33 | public UIElementGroup(UIElementHost host) { 34 | this(host, null); 35 | } 36 | 37 | public UIElementGroup(UIElementHost host, AttributeSet attrs) { 38 | super(host, attrs); 39 | mElements = new ArrayList(); 40 | } 41 | 42 | @Override 43 | public boolean swapHost(UIElementHost host) { 44 | boolean changed = super.swapHost(host); 45 | 46 | if (mElements != null) { 47 | for (UIElement element : mElements) { 48 | element.swapHost(host); 49 | } 50 | } 51 | 52 | return changed; 53 | } 54 | 55 | @Override 56 | public void onAttachedToHost() { 57 | super.onAttachedToHost(); 58 | 59 | if (mElements != null) { 60 | for (UIElement element : mElements) { 61 | if (element instanceof AbstractUIElement) { 62 | ((AbstractUIElement) element).onAttachedToHost(); 63 | } 64 | } 65 | } 66 | } 67 | 68 | @Override 69 | public void onDetachedFromHost() { 70 | super.onDetachedFromHost(); 71 | 72 | for (UIElement element : mElements) { 73 | if (element instanceof AbstractUIElement) { 74 | ((AbstractUIElement) element).onDetachedFromHost(); 75 | } 76 | } 77 | } 78 | 79 | @Override 80 | public void onDraw(Canvas canvas) { 81 | final int saveCount = canvas.getSaveCount(); 82 | canvas.save(); 83 | 84 | for (UIElement element : mElements) { 85 | if (element.getVisibility() == View.VISIBLE) { 86 | element.draw(canvas); 87 | } 88 | } 89 | 90 | canvas.restoreToCount(saveCount); 91 | } 92 | 93 | @Override 94 | public void drawableStateChanged() { 95 | for (UIElement element : mElements) { 96 | element.drawableStateChanged(); 97 | } 98 | } 99 | 100 | public void addElement(UIElement element) { 101 | LayoutParams lp = element.getLayoutParams(); 102 | if (lp == null) { 103 | lp = generateDefaultLayoutParams(); 104 | } 105 | 106 | addElement(element, lp); 107 | } 108 | 109 | public void addElement(UIElement element, LayoutParams lp) { 110 | if (!checkLayoutParams(lp)) { 111 | lp = generateLayoutParams(lp); 112 | } 113 | 114 | element.setLayoutParams(lp); 115 | mElements.add(element); 116 | requestLayout(); 117 | } 118 | 119 | public void removeElement(UIElement element) { 120 | mElements.remove(element); 121 | requestLayout(); 122 | } 123 | 124 | public UIElement findElementById(int id) { 125 | for (UIElement element : mElements) { 126 | if (element.getId() == id) { 127 | return element; 128 | } 129 | } 130 | 131 | return null; 132 | } 133 | 134 | protected boolean checkLayoutParams(LayoutParams lp) { 135 | return (lp != null && lp instanceof MarginLayoutParams); 136 | } 137 | 138 | protected LayoutParams generateLayoutParams(LayoutParams lp) { 139 | if (lp == null) { 140 | return generateDefaultLayoutParams(); 141 | } 142 | 143 | return new MarginLayoutParams(lp.width, lp.height); 144 | } 145 | 146 | public LayoutParams generateLayoutParams(AttributeSet attrs) { 147 | return new MarginLayoutParams(getContext(), attrs); 148 | } 149 | 150 | protected LayoutParams generateDefaultLayoutParams() { 151 | return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 152 | } 153 | 154 | protected void measureElementWithMargins(UIElement element, 155 | int parentWidthMeasureSpec, int widthUsed, 156 | int parentHeightMeasureSpec, int heightUsed) { 157 | final MarginLayoutParams lp = (MarginLayoutParams) element.getLayoutParams(); 158 | 159 | final int childWidthMeasureSpec = getElementMeasureSpec(parentWidthMeasureSpec, 160 | getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin 161 | + widthUsed, lp.width 162 | ); 163 | 164 | final int childHeightMeasureSpec = getElementMeasureSpec(parentHeightMeasureSpec, 165 | getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin 166 | + heightUsed, lp.height 167 | ); 168 | 169 | element.measure(childWidthMeasureSpec, childHeightMeasureSpec); 170 | } 171 | 172 | protected static int getElementMeasureSpec(int spec, int padding, int childDimension) { 173 | int specMode = MeasureSpec.getMode(spec); 174 | int specSize = MeasureSpec.getSize(spec); 175 | 176 | int size = Math.max(0, specSize - padding); 177 | 178 | int resultSize = 0; 179 | int resultMode = 0; 180 | 181 | switch (specMode) { 182 | // Parent has imposed an exact size on us 183 | case MeasureSpec.EXACTLY: 184 | if (childDimension >= 0) { 185 | resultSize = childDimension; 186 | resultMode = MeasureSpec.EXACTLY; 187 | } else if (childDimension == LayoutParams.MATCH_PARENT) { 188 | // Child wants to be our size. So be it. 189 | resultSize = size; 190 | resultMode = MeasureSpec.EXACTLY; 191 | } else if (childDimension == LayoutParams.WRAP_CONTENT) { 192 | // Child wants to determine its own size. It can't be 193 | // bigger than us. 194 | resultSize = size; 195 | resultMode = MeasureSpec.AT_MOST; 196 | } 197 | break; 198 | 199 | // Parent has imposed a maximum size on us 200 | case MeasureSpec.AT_MOST: 201 | if (childDimension >= 0) { 202 | // Child wants a specific size... so be it 203 | resultSize = childDimension; 204 | resultMode = MeasureSpec.EXACTLY; 205 | } else if (childDimension == LayoutParams.MATCH_PARENT) { 206 | // Child wants to be our size, but our size is not fixed. 207 | // Constrain child to not be bigger than us. 208 | resultSize = size; 209 | resultMode = MeasureSpec.AT_MOST; 210 | } else if (childDimension == LayoutParams.WRAP_CONTENT) { 211 | // Child wants to determine its own size. It can't be 212 | // bigger than us. 213 | resultSize = size; 214 | resultMode = MeasureSpec.AT_MOST; 215 | } 216 | break; 217 | 218 | // Parent asked to see how big we want to be 219 | case MeasureSpec.UNSPECIFIED: 220 | if (childDimension >= 0) { 221 | // Child wants a specific size... let him have it 222 | resultSize = childDimension; 223 | resultMode = MeasureSpec.EXACTLY; 224 | } else if (childDimension == LayoutParams.MATCH_PARENT) { 225 | // Child wants to be our size... find out how big it should 226 | // be 227 | resultSize = 0; 228 | resultMode = MeasureSpec.UNSPECIFIED; 229 | } else if (childDimension == LayoutParams.WRAP_CONTENT) { 230 | // Child wants to determine its own size.... find out how 231 | // big it should be 232 | resultSize = 0; 233 | resultMode = View.MeasureSpec.UNSPECIFIED; 234 | } 235 | break; 236 | } 237 | 238 | return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/UIElementHost.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.canvas; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.drawable.Drawable; 22 | import android.view.ViewGroup.LayoutParams; 23 | 24 | public interface UIElementHost { 25 | public void requestLayout(); 26 | 27 | public void invalidate(); 28 | public void invalidate(int left, int top, int right, int bottom); 29 | 30 | public int[] getDrawableState(); 31 | 32 | public Context getContext(); 33 | public Resources getResources(); 34 | 35 | public void invalidateDrawable(Drawable who); 36 | public void scheduleDrawable(Drawable who, Runnable what, long when); 37 | public void unscheduleDrawable(Drawable who); 38 | public void unscheduleDrawable(Drawable who, Runnable what); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/UIElementInflater.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * Copyright (C) 2014 Lucas Rocha 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package org.lucasr.layoutsamples.canvas; 19 | 20 | import android.util.Log; 21 | import android.view.InflateException; 22 | import android.view.ViewGroup.LayoutParams; 23 | import org.xmlpull.v1.XmlPullParser; 24 | import org.xmlpull.v1.XmlPullParserException; 25 | 26 | import android.content.Context; 27 | import android.content.res.XmlResourceParser; 28 | import android.util.AttributeSet; 29 | import android.util.Xml; 30 | 31 | import java.io.IOException; 32 | import java.lang.reflect.Constructor; 33 | import java.util.HashMap; 34 | 35 | public class UIElementInflater { 36 | private static final String LOGTAG = "UIElementInflater"; 37 | private static final boolean DEBUG = false; 38 | 39 | private final Context mContext; 40 | private final Object[] mConstructorArgs = new Object[2]; 41 | 42 | private static UIElementInflater sInstance; 43 | 44 | private static final Class[] sConstructorSignature = new Class[] { 45 | UIElementHost.class, 46 | AttributeSet.class 47 | }; 48 | 49 | private static final HashMap> sConstructorMap = 50 | new HashMap>(); 51 | 52 | private static final String TAG_MERGE = "merge"; 53 | 54 | public static synchronized UIElementInflater from(Context context) { 55 | if (sInstance == null) { 56 | sInstance = new UIElementInflater(context.getApplicationContext()); 57 | } 58 | 59 | return sInstance; 60 | } 61 | 62 | protected UIElementInflater(Context context) { 63 | mContext = context; 64 | } 65 | 66 | public Context getContext() { 67 | return mContext; 68 | } 69 | 70 | public UIElement inflate(int resource, UIElementHost host, UIElementGroup root) { 71 | return inflate(resource, host, root, root != null); 72 | } 73 | 74 | public UIElement inflate(XmlPullParser parser, UIElementHost host, UIElementGroup root) { 75 | return inflate(parser, host, root, root != null); 76 | } 77 | 78 | public UIElement inflate(int resource, UIElementHost host, UIElementGroup root, 79 | boolean attachToRoot) { 80 | if (DEBUG) { 81 | Log.d(LOGTAG, "INFLATING from resource: " + resource); 82 | } 83 | 84 | XmlResourceParser parser = getContext().getResources().getLayout(resource); 85 | try { 86 | return inflate(parser, host, root, attachToRoot); 87 | } finally { 88 | parser.close(); 89 | } 90 | } 91 | 92 | public UIElement inflate(XmlPullParser parser, UIElementHost host, 93 | UIElementGroup root, boolean attachToRoot) { 94 | synchronized (mConstructorArgs) { 95 | final AttributeSet attrs = Xml.asAttributeSet(parser); 96 | mConstructorArgs[0] = host; 97 | 98 | UIElement result = root; 99 | 100 | try { 101 | // Look for the root node. 102 | int type; 103 | while ((type = parser.next()) != XmlPullParser.START_TAG && 104 | type != XmlPullParser.END_DOCUMENT) { 105 | // Empty 106 | } 107 | 108 | if (type != XmlPullParser.START_TAG) { 109 | throw new InflateException(parser.getPositionDescription() 110 | + ": No start tag found!"); 111 | } 112 | 113 | final String name = parser.getName(); 114 | 115 | if (DEBUG) { 116 | Log.d(LOGTAG, "**************************"); 117 | Log.d(LOGTAG, "Creating root view: " + name); 118 | Log.d(LOGTAG, "**************************"); 119 | } 120 | 121 | if (TAG_MERGE.equals(name)) { 122 | if (root == null || !attachToRoot) { 123 | throw new InflateException(" can be used only with a valid " 124 | + "ViewGroup root and attachToRoot=true"); 125 | } 126 | 127 | rInflate(parser, root, attrs, false); 128 | } else { 129 | // Temp is the root view that was found in the xml 130 | UIElement temp = createViewFromTag(root, name, attrs); 131 | LayoutParams params = null; 132 | 133 | if (root != null) { 134 | if (DEBUG) { 135 | Log.d(LOGTAG, "Creating params from root: " + root); 136 | } 137 | 138 | // Create layout params that match root, if supplied 139 | params = root.generateLayoutParams(attrs); 140 | if (!attachToRoot) { 141 | // Set the layout params for temp if we are not 142 | // attaching. (If we are, we use addView, below) 143 | temp.setLayoutParams(params); 144 | } 145 | } 146 | 147 | if (DEBUG) { 148 | Log.d(LOGTAG, "-----> start inflating children"); 149 | } 150 | 151 | // Inflate all children under temp 152 | rInflate(parser, temp, attrs, true); 153 | if (DEBUG) { 154 | Log.d(LOGTAG, "-----> done inflating children"); 155 | } 156 | 157 | // We are supposed to attach all the views we found (int temp) 158 | // to root. Do that now. 159 | if (root != null && attachToRoot) { 160 | root.addElement(temp, params); 161 | } 162 | 163 | // Decide whether to return the root that was passed in or the 164 | // top view found in xml. 165 | if (root == null || !attachToRoot) { 166 | result = temp; 167 | } 168 | } 169 | } catch (XmlPullParserException e) { 170 | InflateException ex = new InflateException(e.getMessage()); 171 | ex.initCause(e); 172 | throw ex; 173 | } catch (IOException e) { 174 | InflateException ex = new InflateException( 175 | parser.getPositionDescription() 176 | + ": " + e.getMessage()); 177 | ex.initCause(e); 178 | throw ex; 179 | } finally { 180 | // Don't retain static reference on host. 181 | mConstructorArgs[0] = null; 182 | mConstructorArgs[1] = null; 183 | } 184 | 185 | return result; 186 | } 187 | } 188 | 189 | public final UIElement createElement(String name, String prefix, AttributeSet attrs) 190 | throws ClassNotFoundException, InflateException { 191 | Constructor constructor = sConstructorMap.get(name); 192 | Class clazz = null; 193 | 194 | try { 195 | if (constructor == null) { 196 | // Class not found in the cache, see if it's real, and try to add it 197 | clazz = mContext.getClassLoader().loadClass( 198 | prefix != null ? (prefix + name) : name).asSubclass(UIElement.class); 199 | 200 | constructor = clazz.getConstructor(sConstructorSignature); 201 | sConstructorMap.put(name, constructor); 202 | } 203 | 204 | Object[] args = mConstructorArgs; 205 | args[1] = attrs; 206 | 207 | return constructor.newInstance(args); 208 | } catch (NoSuchMethodException e) { 209 | InflateException ie = new InflateException(attrs.getPositionDescription() 210 | + ": Error inflating class " 211 | + (prefix != null ? (prefix + name) : name)); 212 | ie.initCause(e); 213 | throw ie; 214 | } catch (ClassCastException e) { 215 | // If loaded class is not a View subclass 216 | InflateException ie = new InflateException(attrs.getPositionDescription() 217 | + ": Class is not a View " 218 | + (prefix != null ? (prefix + name) : name)); 219 | ie.initCause(e); 220 | throw ie; 221 | } catch (ClassNotFoundException e) { 222 | // If loadClass fails, we should propagate the exception. 223 | throw e; 224 | } catch (Exception e) { 225 | InflateException ie = new InflateException(attrs.getPositionDescription() 226 | + ": Error inflating class " 227 | + (clazz == null ? "" : clazz.getName())); 228 | ie.initCause(e); 229 | throw ie; 230 | } 231 | } 232 | 233 | protected UIElement onCreateElement(String name, AttributeSet attrs) 234 | throws ClassNotFoundException { 235 | return createElement(name, "org.lucasr.layoutsamples.canvas.", attrs); 236 | } 237 | 238 | protected UIElement onCreateElement(UIElement parent, String name, AttributeSet attrs) 239 | throws ClassNotFoundException { 240 | return onCreateElement(name, attrs); 241 | } 242 | 243 | UIElement createViewFromTag(UIElement parent, String name, AttributeSet attrs) { 244 | if (name.equals("element")) { 245 | name = attrs.getAttributeValue(null, "class"); 246 | } 247 | 248 | if (DEBUG) { 249 | Log.d(LOGTAG, "******** Creating view: " + name); 250 | } 251 | 252 | try { 253 | final UIElement element; 254 | if (-1 == name.indexOf('.')) { 255 | element = onCreateElement(parent, name, attrs); 256 | } else { 257 | element = createElement(name, null, attrs); 258 | } 259 | 260 | if (DEBUG) { 261 | Log.d(LOGTAG, "Created view is: " + element); 262 | } 263 | 264 | return element; 265 | } catch (InflateException e) { 266 | throw e; 267 | } catch (ClassNotFoundException e) { 268 | InflateException ie = new InflateException(attrs.getPositionDescription() 269 | + ": Error inflating class " + name); 270 | ie.initCause(e); 271 | throw ie; 272 | } catch (Exception e) { 273 | InflateException ie = new InflateException(attrs.getPositionDescription() 274 | + ": Error inflating class " + name); 275 | ie.initCause(e); 276 | throw ie; 277 | } 278 | } 279 | 280 | void rInflate(XmlPullParser parser, UIElement parent, final AttributeSet attrs, 281 | boolean finishInflate) throws XmlPullParserException, IOException { 282 | final int depth = parser.getDepth(); 283 | int type; 284 | 285 | while (((type = parser.next()) != XmlPullParser.END_TAG || 286 | parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 287 | 288 | if (type != XmlPullParser.START_TAG) { 289 | continue; 290 | } 291 | 292 | final String name = parser.getName(); 293 | 294 | if (TAG_MERGE.equals(name)) { 295 | throw new InflateException(" must be the root element"); 296 | } else { 297 | final UIElement element = createViewFromTag(parent, name, attrs); 298 | final UIElementGroup elementGroup = (UIElementGroup) parent; 299 | final LayoutParams params = elementGroup.generateLayoutParams(attrs); 300 | rInflate(parser, element, attrs, true); 301 | elementGroup.addElement(element, params); 302 | } 303 | } 304 | 305 | if (finishInflate) { 306 | parent.onFinishInflate(); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/UIElementView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.canvas; 18 | 19 | import android.content.Context; 20 | import android.graphics.Canvas; 21 | import android.util.AttributeSet; 22 | import android.view.View; 23 | 24 | public class UIElementView extends View implements UIElementHost { 25 | private UIElement mUIElement; 26 | 27 | public UIElementView(Context context, AttributeSet attrs) { 28 | this(context, attrs, 0); 29 | } 30 | 31 | public UIElementView(Context context, AttributeSet attrs, int defStyleAttr) { 32 | super(context, attrs, defStyleAttr); 33 | } 34 | 35 | @Override 36 | protected void onAttachedToWindow() { 37 | super.onAttachedToWindow(); 38 | 39 | if (mUIElement != null) { 40 | mUIElement.swapHost(this); 41 | } 42 | } 43 | 44 | @Override 45 | protected void onDetachedFromWindow() { 46 | super.onDetachedFromWindow(); 47 | 48 | if (mUIElement != null) { 49 | mUIElement.swapHost(null); 50 | } 51 | } 52 | 53 | @Override 54 | protected void onDraw(Canvas canvas) { 55 | super.onDraw(canvas); 56 | 57 | final int saveCount = canvas.getSaveCount(); 58 | canvas.save(); 59 | 60 | if (mUIElement != null) { 61 | mUIElement.draw(canvas); 62 | } 63 | 64 | canvas.restoreToCount(saveCount); 65 | } 66 | 67 | @Override 68 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 69 | int measuredWidth = 0; 70 | int measuredHeight = 0; 71 | 72 | if (mUIElement != null) { 73 | final int paddingLeft = getPaddingLeft(); 74 | final int paddingTop = getPaddingTop(); 75 | final int paddingRight = getPaddingRight(); 76 | final int paddingBottom = getPaddingBottom(); 77 | 78 | final int viewWidthSize = MeasureSpec.getSize(widthMeasureSpec); 79 | final int viewWidthMode = MeasureSpec.getMode(widthMeasureSpec); 80 | final int viewHeightSize = MeasureSpec.getSize(heightMeasureSpec); 81 | final int viewHeightMode = MeasureSpec.getMode(heightMeasureSpec); 82 | 83 | final int elementWidth = viewWidthSize - paddingLeft - paddingRight; 84 | final int elementWidthSpec = MeasureSpec.makeMeasureSpec(elementWidth, viewWidthMode); 85 | final int elementHeight = viewHeightSize - paddingTop - paddingBottom; 86 | final int elementHeightSpec = MeasureSpec.makeMeasureSpec(elementHeight, viewHeightMode); 87 | 88 | mUIElement.measure(elementWidthSpec, elementHeightSpec); 89 | 90 | measuredWidth = mUIElement.getMeasuredWidth() + paddingLeft + paddingRight; 91 | measuredHeight = mUIElement.getMeasuredHeight() + paddingTop + paddingBottom; 92 | } 93 | 94 | measuredWidth = Math.max(measuredWidth, getSuggestedMinimumWidth()); 95 | measuredHeight = Math.max(measuredHeight, getSuggestedMinimumHeight()); 96 | 97 | setMeasuredDimension(measuredWidth, measuredHeight); 98 | } 99 | 100 | @Override 101 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 102 | super.onLayout(changed, left, top, right, bottom); 103 | 104 | if (mUIElement != null) { 105 | final int paddingLeft = getPaddingLeft(); 106 | final int paddingTop = getPaddingTop(); 107 | 108 | final int elementLeft = paddingLeft; 109 | final int elementTop = paddingTop; 110 | final int elementRight = right - left - getPaddingRight(); 111 | final int elementBottom = bottom - top - getPaddingBottom(); 112 | 113 | mUIElement.layout(elementLeft, elementTop, elementRight, elementBottom); 114 | } 115 | } 116 | 117 | @Override 118 | public void drawableStateChanged() { 119 | if (mUIElement != null) { 120 | mUIElement.drawableStateChanged(); 121 | } 122 | } 123 | 124 | public UIElement getUIElement() { 125 | return mUIElement; 126 | } 127 | 128 | public void setUIElement(UIElement element) { 129 | if (mUIElement == element) { 130 | return; 131 | } 132 | 133 | if (mUIElement != null) { 134 | mUIElement.swapHost(null); 135 | } 136 | 137 | mUIElement = element; 138 | 139 | if (mUIElement != null) { 140 | mUIElement.swapHost(this); 141 | } 142 | 143 | requestLayout(); 144 | invalidate(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/canvas/UIElementWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.canvas; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | import android.graphics.Canvas; 22 | import android.view.ViewGroup; 23 | 24 | public class UIElementWrapper implements UIElement { 25 | private final UIElement mWrappedElement; 26 | 27 | public UIElementWrapper(UIElement element) { 28 | mWrappedElement = element; 29 | } 30 | 31 | @Override 32 | public boolean swapHost(UIElementHost host) { 33 | return mWrappedElement.swapHost(host); 34 | } 35 | 36 | @Override 37 | public boolean isAttachedToHost() { 38 | return mWrappedElement.isAttachedToHost(); 39 | } 40 | 41 | @Override 42 | public int getId() { 43 | return mWrappedElement.getId(); 44 | } 45 | 46 | @Override 47 | public int getMeasuredWidth() { 48 | return mWrappedElement.getMeasuredWidth(); 49 | } 50 | 51 | @Override 52 | public int getMeasuredHeight() { 53 | return mWrappedElement.getMeasuredHeight(); 54 | } 55 | 56 | @Override 57 | public int getPaddingLeft() { 58 | return mWrappedElement.getPaddingLeft(); 59 | } 60 | 61 | @Override 62 | public int getPaddingTop() { 63 | return mWrappedElement.getPaddingTop(); 64 | } 65 | 66 | @Override 67 | public int getPaddingRight() { 68 | return mWrappedElement.getPaddingRight(); 69 | } 70 | 71 | @Override 72 | public int getPaddingBottom() { 73 | return mWrappedElement.getPaddingBottom(); 74 | } 75 | 76 | @Override 77 | public void setPadding(int left, int top, int right, int bottom) { 78 | mWrappedElement.setPadding(left, top, right, bottom); 79 | } 80 | 81 | @Override 82 | public int getLeft() { 83 | return mWrappedElement.getLeft(); 84 | } 85 | 86 | @Override 87 | public int getTop() { 88 | return mWrappedElement.getTop(); 89 | } 90 | 91 | @Override 92 | public int getRight() { 93 | return mWrappedElement.getRight(); 94 | } 95 | 96 | @Override 97 | public int getBottom() { 98 | return mWrappedElement.getBottom(); 99 | } 100 | 101 | @Override 102 | public int getWidth() { 103 | return mWrappedElement.getWidth(); 104 | } 105 | 106 | @Override 107 | public int getHeight() { 108 | return mWrappedElement.getHeight(); 109 | } 110 | 111 | @Override 112 | public void setLayoutParams(ViewGroup.LayoutParams lp) { 113 | mWrappedElement.setLayoutParams(lp); 114 | } 115 | 116 | @Override 117 | public ViewGroup.LayoutParams getLayoutParams() { 118 | return mWrappedElement.getLayoutParams(); 119 | } 120 | 121 | @Override 122 | public void onFinishInflate() { 123 | mWrappedElement.onFinishInflate(); 124 | } 125 | 126 | @Override 127 | public void measure(int widthMeasureSpec, int heightMeasureSpec) { 128 | mWrappedElement.measure(widthMeasureSpec, heightMeasureSpec); 129 | } 130 | 131 | @Override 132 | public void layout(int left, int top, int right, int bottom) { 133 | mWrappedElement.layout(left, top, right, bottom); 134 | } 135 | 136 | @Override 137 | public void draw(Canvas canvas) { 138 | mWrappedElement.draw(canvas); 139 | } 140 | 141 | @Override 142 | public void drawableStateChanged() { 143 | mWrappedElement.drawableStateChanged(); 144 | } 145 | 146 | @Override 147 | public Context getContext() { 148 | return mWrappedElement.getContext(); 149 | } 150 | 151 | @Override 152 | public Resources getResources() { 153 | return mWrappedElement.getResources(); 154 | } 155 | 156 | @Override 157 | public void requestLayout() { 158 | mWrappedElement.requestLayout(); 159 | } 160 | 161 | @Override 162 | public void invalidate() { 163 | mWrappedElement.invalidate(); 164 | } 165 | 166 | @Override 167 | public int getVisibility() { 168 | return mWrappedElement.getVisibility(); 169 | } 170 | 171 | @Override 172 | public void setVisibility(int visibility) { 173 | mWrappedElement.setVisibility(visibility); 174 | } 175 | 176 | public UIElement getWrappedElement() { 177 | return mWrappedElement; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/util/ImageUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.util; 18 | 19 | import android.content.Context; 20 | import android.widget.ImageView; 21 | 22 | import com.squareup.picasso.Picasso; 23 | 24 | import org.lucasr.layoutsamples.adapter.TweetPresenter.UpdateFlags; 25 | import org.lucasr.layoutsamples.app.R; 26 | import org.lucasr.layoutsamples.canvas.ImageElement; 27 | import org.lucasr.layoutsamples.widget.ImageElementTarget; 28 | 29 | import java.util.EnumSet; 30 | 31 | public class ImageUtils { 32 | private ImageUtils() { 33 | } 34 | 35 | public static void loadImage(Context context, ImageView view, String url, 36 | EnumSet flags) { 37 | if (!flags.contains(UpdateFlags.NO_IMAGE_LOADING)) { 38 | Picasso.with(context) 39 | .load(url) 40 | .placeholder(R.drawable.tweet_placeholder_image) 41 | .error(R.drawable.tweet_placeholder_image) 42 | .into(view); 43 | } else { 44 | view.setImageResource(R.drawable.tweet_placeholder_image); 45 | } 46 | } 47 | 48 | public static void loadImage(Context context, ImageElement element, 49 | ImageElementTarget target, String url, 50 | EnumSet flags) { 51 | if (!flags.contains(UpdateFlags.NO_IMAGE_LOADING)) { 52 | Picasso.with(context) 53 | .load(url) 54 | .placeholder(R.drawable.tweet_placeholder_image) 55 | .error(R.drawable.tweet_placeholder_image) 56 | .into(target); 57 | } else { 58 | element.setImageResource(R.drawable.tweet_placeholder_image); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/util/RawResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.util; 18 | 19 | import android.content.Context; 20 | import android.content.res.Resources; 21 | 22 | import org.json.JSONArray; 23 | import org.json.JSONException; 24 | 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.io.InputStreamReader; 28 | import java.io.StringWriter; 29 | 30 | public final class RawResource { 31 | public static JSONArray getAsJSON(Context context, int id) throws IOException { 32 | InputStreamReader reader = null; 33 | 34 | try { 35 | final Resources res = context.getResources(); 36 | final InputStream is = res.openRawResource(id); 37 | if (is == null) { 38 | return null; 39 | } 40 | 41 | reader = new InputStreamReader(is); 42 | 43 | final char[] buffer = new char[1024]; 44 | final StringWriter s = new StringWriter(); 45 | 46 | int n; 47 | while ((n = reader.read(buffer, 0, buffer.length)) != -1) { 48 | s.write(buffer, 0, n); 49 | } 50 | 51 | return new JSONArray(s.toString()); 52 | } catch (JSONException e) { 53 | return null; 54 | } finally { 55 | if (reader != null) { 56 | reader.close(); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/util/ViewServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.util; 18 | 19 | import java.io.BufferedReader; 20 | import java.io.BufferedWriter; 21 | import java.io.IOException; 22 | import java.io.InputStreamReader; 23 | import java.io.OutputStream; 24 | import java.io.OutputStreamWriter; 25 | import java.lang.reflect.Method; 26 | import java.net.InetAddress; 27 | import java.net.ServerSocket; 28 | import java.net.Socket; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map.Entry; 32 | import java.util.concurrent.CopyOnWriteArrayList; 33 | import java.util.concurrent.ExecutorService; 34 | import java.util.concurrent.Executors; 35 | import java.util.concurrent.locks.ReentrantReadWriteLock; 36 | 37 | import android.app.Activity; 38 | import android.content.Context; 39 | import android.content.pm.ApplicationInfo; 40 | import android.os.Build; 41 | import android.text.TextUtils; 42 | import android.util.Log; 43 | import android.view.View; 44 | import android.view.ViewDebug; 45 | 46 | /** 47 | *

This class can be used to enable the use of HierarchyViewer inside an 48 | * application. HierarchyViewer is an Android SDK tool that can be used 49 | * to inspect and debug the user interface of running applications. For 50 | * security reasons, HierarchyViewer does not work on production builds 51 | * (for instance phones bought in store.) By using this class, you can 52 | * make HierarchyViewer work on any device. You must be very careful 53 | * however to only enable HierarchyViewer when debugging your 54 | * application.

55 | * 56 | *

To use this view server, your application must require the INTERNET 57 | * permission.

58 | * 59 | *

The recommended way to use this API is to register activities when 60 | * they are created, and to unregister them when they get destroyed:

61 | * 62 | *
 63 |  * public class MyActivity extends Activity {
 64 |  *     public void onCreate(Bundle savedInstanceState) {
 65 |  *         super.onCreate(savedInstanceState);
 66 |  *         // Set content view, etc.
 67 |  *         ViewServer.get(this).addWindow(this);
 68 |  *     }
 69 |  *
 70 |  *     public void onDestroy() {
 71 |  *         super.onDestroy();
 72 |  *         ViewServer.get(this).removeWindow(this);
 73 |  *     }
 74 |  *
 75 |  *     public void onResume() {
 76 |  *         super.onResume();
 77 |  *         ViewServer.get(this).setFocusedWindow(this);
 78 |  *     }
 79 |  * }
 80 |  * 
81 | * 82 | *

83 | * In a similar fashion, you can use this API with an InputMethodService: 84 | *

85 | * 86 | *
 87 |  * public class MyInputMethodService extends InputMethodService {
 88 |  *     public void onCreate() {
 89 |  *         super.onCreate();
 90 |  *         View decorView = getWindow().getWindow().getDecorView();
 91 |  *         String name = "MyInputMethodService";
 92 |  *         ViewServer.get(this).addWindow(decorView, name);
 93 |  *     }
 94 |  *
 95 |  *     public void onDestroy() {
 96 |  *         super.onDestroy();
 97 |  *         View decorView = getWindow().getWindow().getDecorView();
 98 |  *         ViewServer.get(this).removeWindow(decorView);
 99 |  *     }
100 |  *
101 |  *     public void onStartInput(EditorInfo attribute, boolean restarting) {
102 |  *         super.onStartInput(attribute, restarting);
103 |  *         View decorView = getWindow().getWindow().getDecorView();
104 |  *         ViewServer.get(this).setFocusedWindow(decorView);
105 |  *     }
106 |  * }
107 |  * 
108 | */ 109 | public class ViewServer implements Runnable { 110 | /** 111 | * The default port used to start view servers. 112 | */ 113 | private static final int VIEW_SERVER_DEFAULT_PORT = 4939; 114 | private static final int VIEW_SERVER_MAX_CONNECTIONS = 10; 115 | private static final String BUILD_TYPE_USER = "user"; 116 | 117 | // Debug facility 118 | private static final String LOG_TAG = "ViewServer"; 119 | 120 | private static final String VALUE_PROTOCOL_VERSION = "4"; 121 | private static final String VALUE_SERVER_VERSION = "4"; 122 | 123 | // Protocol commands 124 | // Returns the protocol version 125 | private static final String COMMAND_PROTOCOL_VERSION = "PROTOCOL"; 126 | // Returns the server version 127 | private static final String COMMAND_SERVER_VERSION = "SERVER"; 128 | // Lists all of the available windows in the system 129 | private static final String COMMAND_WINDOW_MANAGER_LIST = "LIST"; 130 | // Keeps a connection open and notifies when the list of windows changes 131 | private static final String COMMAND_WINDOW_MANAGER_AUTOLIST = "AUTOLIST"; 132 | // Returns the focused window 133 | private static final String COMMAND_WINDOW_MANAGER_GET_FOCUS = "GET_FOCUS"; 134 | 135 | private ServerSocket mServer; 136 | private final int mPort; 137 | 138 | private Thread mThread; 139 | private ExecutorService mThreadPool; 140 | 141 | private final List mListeners = 142 | new CopyOnWriteArrayList(); 143 | 144 | private final HashMap mWindows = new HashMap(); 145 | private final ReentrantReadWriteLock mWindowsLock = new ReentrantReadWriteLock(); 146 | 147 | private View mFocusedWindow; 148 | private final ReentrantReadWriteLock mFocusLock = new ReentrantReadWriteLock(); 149 | 150 | private static ViewServer sServer; 151 | 152 | /** 153 | * Returns a unique instance of the ViewServer. This method should only be 154 | * called from the main thread of your application. The server will have 155 | * the same lifetime as your process. 156 | * 157 | * If your application does not have the android:debuggable 158 | * flag set in its manifest, the server returned by this method will 159 | * be a dummy object that does not do anything. This allows you to use 160 | * the same code in debug and release versions of your application. 161 | * 162 | * @param context A Context used to check whether the application is 163 | * debuggable, this can be the application context 164 | */ 165 | public static ViewServer get(Context context) { 166 | ApplicationInfo info = context.getApplicationInfo(); 167 | if (BUILD_TYPE_USER.equals(Build.TYPE) && 168 | (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { 169 | if (sServer == null) { 170 | sServer = new ViewServer(ViewServer.VIEW_SERVER_DEFAULT_PORT); 171 | } 172 | 173 | if (!sServer.isRunning()) { 174 | try { 175 | sServer.start(); 176 | } catch (IOException e) { 177 | Log.d(LOG_TAG, "Error:", e); 178 | } 179 | } 180 | } else { 181 | sServer = new NoopViewServer(); 182 | } 183 | 184 | return sServer; 185 | } 186 | 187 | private ViewServer() { 188 | mPort = -1; 189 | } 190 | 191 | /** 192 | * Creates a new ViewServer associated with the specified window manager on the 193 | * specified local port. The server is not started by default. 194 | * 195 | * @param port The port for the server to listen to. 196 | * 197 | * @see #start() 198 | */ 199 | private ViewServer(int port) { 200 | mPort = port; 201 | } 202 | 203 | /** 204 | * Starts the server. 205 | * 206 | * @return True if the server was successfully created, or false if it already exists. 207 | * @throws IOException If the server cannot be created. 208 | * 209 | * @see #stop() 210 | * @see #isRunning() 211 | */ 212 | public boolean start() throws IOException { 213 | if (mThread != null) { 214 | return false; 215 | } 216 | 217 | mThread = new Thread(this, "Local View Server [port=" + mPort + "]"); 218 | mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS); 219 | mThread.start(); 220 | 221 | return true; 222 | } 223 | 224 | /** 225 | * Stops the server. 226 | * 227 | * @return True if the server was stopped, false if an error occurred or if the 228 | * server wasn't started. 229 | * 230 | * @see #start() 231 | * @see #isRunning() 232 | */ 233 | public boolean stop() { 234 | if (mThread != null) { 235 | mThread.interrupt(); 236 | if (mThreadPool != null) { 237 | try { 238 | mThreadPool.shutdownNow(); 239 | } catch (SecurityException e) { 240 | Log.w(LOG_TAG, "Could not stop all view server threads"); 241 | } 242 | } 243 | 244 | mThreadPool = null; 245 | mThread = null; 246 | 247 | try { 248 | mServer.close(); 249 | mServer = null; 250 | return true; 251 | } catch (IOException e) { 252 | Log.w(LOG_TAG, "Could not close the view server"); 253 | } 254 | } 255 | 256 | mWindowsLock.writeLock().lock(); 257 | try { 258 | mWindows.clear(); 259 | } finally { 260 | mWindowsLock.writeLock().unlock(); 261 | } 262 | 263 | mFocusLock.writeLock().lock(); 264 | try { 265 | mFocusedWindow = null; 266 | } finally { 267 | mFocusLock.writeLock().unlock(); 268 | } 269 | 270 | return false; 271 | } 272 | 273 | /** 274 | * Indicates whether the server is currently running. 275 | * 276 | * @return True if the server is running, false otherwise. 277 | * 278 | * @see #start() 279 | * @see #stop() 280 | */ 281 | public boolean isRunning() { 282 | return mThread != null && mThread.isAlive(); 283 | } 284 | 285 | /** 286 | * Invoke this method to register a new view hierarchy. 287 | * 288 | * @param activity The activity whose view hierarchy/window to register 289 | * 290 | * @see #addWindow(View, String) 291 | * @see #removeWindow(Activity) 292 | */ 293 | public void addWindow(Activity activity) { 294 | String name = activity.getTitle().toString(); 295 | if (TextUtils.isEmpty(name)) { 296 | name = activity.getClass().getCanonicalName() + 297 | "/0x" + System.identityHashCode(activity); 298 | } else { 299 | name += "(" + activity.getClass().getCanonicalName() + ")"; 300 | } 301 | addWindow(activity.getWindow().getDecorView(), name); 302 | } 303 | 304 | /** 305 | * Invoke this method to unregister a view hierarchy. 306 | * 307 | * @param activity The activity whose view hierarchy/window to unregister 308 | * 309 | * @see #addWindow(Activity) 310 | * @see #removeWindow(View) 311 | */ 312 | public void removeWindow(Activity activity) { 313 | removeWindow(activity.getWindow().getDecorView()); 314 | } 315 | 316 | /** 317 | * Invoke this method to register a new view hierarchy. 318 | * 319 | * @param view A view that belongs to the view hierarchy/window to register 320 | * @name name The name of the view hierarchy/window to register 321 | * 322 | * @see #removeWindow(View) 323 | */ 324 | public void addWindow(View view, String name) { 325 | mWindowsLock.writeLock().lock(); 326 | try { 327 | mWindows.put(view.getRootView(), name); 328 | } finally { 329 | mWindowsLock.writeLock().unlock(); 330 | } 331 | fireWindowsChangedEvent(); 332 | } 333 | 334 | /** 335 | * Invoke this method to unregister a view hierarchy. 336 | * 337 | * @param view A view that belongs to the view hierarchy/window to unregister 338 | * 339 | * @see #addWindow(View, String) 340 | */ 341 | public void removeWindow(View view) { 342 | mWindowsLock.writeLock().lock(); 343 | try { 344 | mWindows.remove(view.getRootView()); 345 | } finally { 346 | mWindowsLock.writeLock().unlock(); 347 | } 348 | fireWindowsChangedEvent(); 349 | } 350 | 351 | /** 352 | * Invoke this method to change the currently focused window. 353 | * 354 | * @param activity The activity whose view hierarchy/window hasfocus, 355 | * or null to remove focus 356 | */ 357 | public void setFocusedWindow(Activity activity) { 358 | setFocusedWindow(activity.getWindow().getDecorView()); 359 | } 360 | 361 | /** 362 | * Invoke this method to change the currently focused window. 363 | * 364 | * @param view A view that belongs to the view hierarchy/window that has focus, 365 | * or null to remove focus 366 | */ 367 | public void setFocusedWindow(View view) { 368 | mFocusLock.writeLock().lock(); 369 | try { 370 | mFocusedWindow = view == null ? null : view.getRootView(); 371 | } finally { 372 | mFocusLock.writeLock().unlock(); 373 | } 374 | fireFocusChangedEvent(); 375 | } 376 | 377 | /** 378 | * Main server loop. 379 | */ 380 | public void run() { 381 | try { 382 | mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, InetAddress.getLocalHost()); 383 | } catch (Exception e) { 384 | Log.w(LOG_TAG, "Starting ServerSocket error: ", e); 385 | } 386 | 387 | while (mServer != null && Thread.currentThread() == mThread) { 388 | // Any uncaught exception will crash the system process 389 | try { 390 | Socket client = mServer.accept(); 391 | if (mThreadPool != null) { 392 | mThreadPool.submit(new ViewServerWorker(client)); 393 | } else { 394 | try { 395 | client.close(); 396 | } catch (IOException e) { 397 | e.printStackTrace(); 398 | } 399 | } 400 | } catch (Exception e) { 401 | Log.w(LOG_TAG, "Connection error: ", e); 402 | } 403 | } 404 | } 405 | 406 | private static boolean writeValue(Socket client, String value) { 407 | boolean result; 408 | BufferedWriter out = null; 409 | try { 410 | OutputStream clientStream = client.getOutputStream(); 411 | out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024); 412 | out.write(value); 413 | out.write("\n"); 414 | out.flush(); 415 | result = true; 416 | } catch (Exception e) { 417 | result = false; 418 | } finally { 419 | if (out != null) { 420 | try { 421 | out.close(); 422 | } catch (IOException e) { 423 | result = false; 424 | } 425 | } 426 | } 427 | return result; 428 | } 429 | 430 | private void fireWindowsChangedEvent() { 431 | for (WindowListener listener : mListeners) { 432 | listener.windowsChanged(); 433 | } 434 | } 435 | 436 | private void fireFocusChangedEvent() { 437 | for (WindowListener listener : mListeners) { 438 | listener.focusChanged(); 439 | } 440 | } 441 | 442 | private void addWindowListener(WindowListener listener) { 443 | if (!mListeners.contains(listener)) { 444 | mListeners.add(listener); 445 | } 446 | } 447 | 448 | private void removeWindowListener(WindowListener listener) { 449 | mListeners.remove(listener); 450 | } 451 | 452 | private interface WindowListener { 453 | void windowsChanged(); 454 | void focusChanged(); 455 | } 456 | 457 | private static class UncloseableOutputStream extends OutputStream { 458 | private final OutputStream mStream; 459 | 460 | UncloseableOutputStream(OutputStream stream) { 461 | mStream = stream; 462 | } 463 | 464 | public void close() throws IOException { 465 | // Don't close the stream 466 | } 467 | 468 | public boolean equals(Object o) { 469 | return mStream.equals(o); 470 | } 471 | 472 | public void flush() throws IOException { 473 | mStream.flush(); 474 | } 475 | 476 | public int hashCode() { 477 | return mStream.hashCode(); 478 | } 479 | 480 | public String toString() { 481 | return mStream.toString(); 482 | } 483 | 484 | public void write(byte[] buffer, int offset, int count) 485 | throws IOException { 486 | mStream.write(buffer, offset, count); 487 | } 488 | 489 | public void write(byte[] buffer) throws IOException { 490 | mStream.write(buffer); 491 | } 492 | 493 | public void write(int oneByte) throws IOException { 494 | mStream.write(oneByte); 495 | } 496 | } 497 | 498 | private static class NoopViewServer extends ViewServer { 499 | private NoopViewServer() { 500 | } 501 | 502 | @Override 503 | public boolean start() throws IOException { 504 | return false; 505 | } 506 | 507 | @Override 508 | public boolean stop() { 509 | return false; 510 | } 511 | 512 | @Override 513 | public boolean isRunning() { 514 | return false; 515 | } 516 | 517 | @Override 518 | public void addWindow(Activity activity) { 519 | } 520 | 521 | @Override 522 | public void removeWindow(Activity activity) { 523 | } 524 | 525 | @Override 526 | public void addWindow(View view, String name) { 527 | } 528 | 529 | @Override 530 | public void removeWindow(View view) { 531 | } 532 | 533 | @Override 534 | public void setFocusedWindow(Activity activity) { 535 | } 536 | 537 | @Override 538 | public void setFocusedWindow(View view) { 539 | } 540 | 541 | @Override 542 | public void run() { 543 | } 544 | } 545 | 546 | private class ViewServerWorker implements Runnable, WindowListener { 547 | private Socket mClient; 548 | private boolean mNeedWindowListUpdate; 549 | private boolean mNeedFocusedWindowUpdate; 550 | 551 | private final Object[] mLock = new Object[0]; 552 | 553 | public ViewServerWorker(Socket client) { 554 | mClient = client; 555 | mNeedWindowListUpdate = false; 556 | mNeedFocusedWindowUpdate = false; 557 | } 558 | 559 | public void run() { 560 | BufferedReader in = null; 561 | try { 562 | in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024); 563 | 564 | final String request = in.readLine(); 565 | 566 | String command; 567 | String parameters; 568 | 569 | int index = request.indexOf(' '); 570 | if (index == -1) { 571 | command = request; 572 | parameters = ""; 573 | } else { 574 | command = request.substring(0, index); 575 | parameters = request.substring(index + 1); 576 | } 577 | 578 | boolean result; 579 | if (COMMAND_PROTOCOL_VERSION.equalsIgnoreCase(command)) { 580 | result = writeValue(mClient, VALUE_PROTOCOL_VERSION); 581 | } else if (COMMAND_SERVER_VERSION.equalsIgnoreCase(command)) { 582 | result = writeValue(mClient, VALUE_SERVER_VERSION); 583 | } else if (COMMAND_WINDOW_MANAGER_LIST.equalsIgnoreCase(command)) { 584 | result = listWindows(mClient); 585 | } else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) { 586 | result = getFocusedWindow(mClient); 587 | } else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) { 588 | result = windowManagerAutolistLoop(); 589 | } else { 590 | result = windowCommand(mClient, command, parameters); 591 | } 592 | 593 | if (!result) { 594 | Log.w(LOG_TAG, "An error occurred with the command: " + command); 595 | } 596 | } catch(IOException e) { 597 | Log.w(LOG_TAG, "Connection error: ", e); 598 | } finally { 599 | if (in != null) { 600 | try { 601 | in.close(); 602 | 603 | } catch (IOException e) { 604 | e.printStackTrace(); 605 | } 606 | } 607 | if (mClient != null) { 608 | try { 609 | mClient.close(); 610 | } catch (IOException e) { 611 | e.printStackTrace(); 612 | } 613 | } 614 | } 615 | } 616 | 617 | private boolean windowCommand(Socket client, String command, String parameters) { 618 | boolean success = true; 619 | BufferedWriter out = null; 620 | 621 | try { 622 | // Find the hash code of the window 623 | int index = parameters.indexOf(' '); 624 | if (index == -1) { 625 | index = parameters.length(); 626 | } 627 | final String code = parameters.substring(0, index); 628 | int hashCode = (int) Long.parseLong(code, 16); 629 | 630 | // Extract the command's parameter after the window description 631 | if (index < parameters.length()) { 632 | parameters = parameters.substring(index + 1); 633 | } else { 634 | parameters = ""; 635 | } 636 | 637 | final View window = findWindow(hashCode); 638 | if (window == null) { 639 | return false; 640 | } 641 | 642 | // call stuff 643 | final Method dispatch = ViewDebug.class.getDeclaredMethod("dispatchCommand", 644 | View.class, String.class, String.class, OutputStream.class); 645 | dispatch.setAccessible(true); 646 | dispatch.invoke(null, window, command, parameters, 647 | new UncloseableOutputStream(client.getOutputStream())); 648 | 649 | if (!client.isOutputShutdown()) { 650 | out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream())); 651 | out.write("DONE\n"); 652 | out.flush(); 653 | } 654 | 655 | } catch (Exception e) { 656 | Log.w(LOG_TAG, "Could not send command " + command + 657 | " with parameters " + parameters, e); 658 | success = false; 659 | } finally { 660 | if (out != null) { 661 | try { 662 | out.close(); 663 | } catch (IOException e) { 664 | success = false; 665 | } 666 | } 667 | } 668 | 669 | return success; 670 | } 671 | 672 | private View findWindow(int hashCode) { 673 | if (hashCode == -1) { 674 | View window = null; 675 | mWindowsLock.readLock().lock(); 676 | try { 677 | window = mFocusedWindow; 678 | } finally { 679 | mWindowsLock.readLock().unlock(); 680 | } 681 | return window; 682 | } 683 | 684 | 685 | mWindowsLock.readLock().lock(); 686 | try { 687 | for (Entry entry : mWindows.entrySet()) { 688 | if (System.identityHashCode(entry.getKey()) == hashCode) { 689 | return entry.getKey(); 690 | } 691 | } 692 | } finally { 693 | mWindowsLock.readLock().unlock(); 694 | } 695 | 696 | return null; 697 | } 698 | 699 | private boolean listWindows(Socket client) { 700 | boolean result = true; 701 | BufferedWriter out = null; 702 | 703 | try { 704 | mWindowsLock.readLock().lock(); 705 | 706 | OutputStream clientStream = client.getOutputStream(); 707 | out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024); 708 | 709 | for (Entry entry : mWindows.entrySet()) { 710 | out.write(Integer.toHexString(System.identityHashCode(entry.getKey()))); 711 | out.write(' '); 712 | out.append(entry.getValue()); 713 | out.write('\n'); 714 | } 715 | 716 | out.write("DONE.\n"); 717 | out.flush(); 718 | } catch (Exception e) { 719 | result = false; 720 | } finally { 721 | mWindowsLock.readLock().unlock(); 722 | 723 | if (out != null) { 724 | try { 725 | out.close(); 726 | } catch (IOException e) { 727 | result = false; 728 | } 729 | } 730 | } 731 | 732 | return result; 733 | } 734 | 735 | private boolean getFocusedWindow(Socket client) { 736 | boolean result = true; 737 | String focusName = null; 738 | 739 | BufferedWriter out = null; 740 | try { 741 | OutputStream clientStream = client.getOutputStream(); 742 | out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024); 743 | 744 | View focusedWindow = null; 745 | 746 | mFocusLock.readLock().lock(); 747 | try { 748 | focusedWindow = mFocusedWindow; 749 | } finally { 750 | mFocusLock.readLock().unlock(); 751 | } 752 | 753 | if (focusedWindow != null) { 754 | mWindowsLock.readLock().lock(); 755 | try { 756 | focusName = mWindows.get(mFocusedWindow); 757 | } finally { 758 | mWindowsLock.readLock().unlock(); 759 | } 760 | 761 | out.write(Integer.toHexString(System.identityHashCode(focusedWindow))); 762 | out.write(' '); 763 | out.append(focusName); 764 | } 765 | out.write('\n'); 766 | out.flush(); 767 | } catch (Exception e) { 768 | result = false; 769 | } finally { 770 | if (out != null) { 771 | try { 772 | out.close(); 773 | } catch (IOException e) { 774 | result = false; 775 | } 776 | } 777 | } 778 | 779 | return result; 780 | } 781 | 782 | public void windowsChanged() { 783 | synchronized (mLock) { 784 | mNeedWindowListUpdate = true; 785 | mLock.notifyAll(); 786 | } 787 | } 788 | 789 | public void focusChanged() { 790 | synchronized (mLock) { 791 | mNeedFocusedWindowUpdate = true; 792 | mLock.notifyAll(); 793 | } 794 | } 795 | 796 | private boolean windowManagerAutolistLoop() { 797 | addWindowListener(this); 798 | BufferedWriter out = null; 799 | try { 800 | out = new BufferedWriter(new OutputStreamWriter(mClient.getOutputStream())); 801 | while (!Thread.interrupted()) { 802 | boolean needWindowListUpdate = false; 803 | boolean needFocusedWindowUpdate = false; 804 | synchronized (mLock) { 805 | while (!mNeedWindowListUpdate && !mNeedFocusedWindowUpdate) { 806 | mLock.wait(); 807 | } 808 | if (mNeedWindowListUpdate) { 809 | mNeedWindowListUpdate = false; 810 | needWindowListUpdate = true; 811 | } 812 | if (mNeedFocusedWindowUpdate) { 813 | mNeedFocusedWindowUpdate = false; 814 | needFocusedWindowUpdate = true; 815 | } 816 | } 817 | if (needWindowListUpdate) { 818 | out.write("LIST UPDATE\n"); 819 | out.flush(); 820 | } 821 | if (needFocusedWindowUpdate) { 822 | out.write("FOCUS UPDATE\n"); 823 | out.flush(); 824 | } 825 | } 826 | } catch (Exception e) { 827 | Log.w(LOG_TAG, "Connection error: ", e); 828 | } finally { 829 | if (out != null) { 830 | try { 831 | out.close(); 832 | } catch (IOException e) { 833 | // Ignore 834 | } 835 | } 836 | removeWindowListener(this); 837 | } 838 | return true; 839 | } 840 | } 841 | } 842 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/widget/ImageElementTarget.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.widget; 18 | 19 | import android.content.res.Resources; 20 | import android.graphics.Bitmap; 21 | import android.graphics.drawable.BitmapDrawable; 22 | import android.graphics.drawable.Drawable; 23 | import android.graphics.drawable.TransitionDrawable; 24 | 25 | import com.squareup.picasso.Picasso; 26 | import com.squareup.picasso.Target; 27 | 28 | import org.lucasr.layoutsamples.app.R; 29 | import org.lucasr.layoutsamples.canvas.ImageElement; 30 | 31 | public class ImageElementTarget implements Target { 32 | private final Resources mResources; 33 | private final ImageElement mElement; 34 | 35 | public ImageElementTarget(Resources resources, ImageElement element) { 36 | mResources = resources; 37 | mElement = element; 38 | } 39 | 40 | @Override 41 | public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) { 42 | boolean shouldFade = (loadedFrom != Picasso.LoadedFrom.MEMORY); 43 | 44 | if (shouldFade) { 45 | Drawable placeholder = 46 | mResources.getDrawable(R.drawable.tweet_placeholder_image); 47 | Drawable bitmapDrawable = new BitmapDrawable(mResources, bitmap); 48 | 49 | TransitionDrawable fadeInDrawable = 50 | new TransitionDrawable(new Drawable[] { placeholder, bitmapDrawable }); 51 | 52 | mElement.setImageDrawable(fadeInDrawable); 53 | fadeInDrawable.startTransition(200); 54 | } else { 55 | mElement.setImageBitmap(bitmap); 56 | } 57 | } 58 | 59 | @Override 60 | public void onBitmapFailed(Drawable drawable) { 61 | mElement.setImageDrawable(drawable); 62 | } 63 | 64 | @Override 65 | public void onPrepareLoad(Drawable drawable) { 66 | mElement.setImageDrawable(drawable); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/widget/TweetCompositeView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.widget; 18 | 19 | import android.content.Context; 20 | import android.text.TextUtils; 21 | import android.util.AttributeSet; 22 | import android.view.LayoutInflater; 23 | import android.view.View; 24 | import android.widget.ImageView; 25 | import android.widget.RelativeLayout; 26 | import android.widget.TextView; 27 | 28 | import org.lucasr.layoutsamples.adapter.Tweet; 29 | import org.lucasr.layoutsamples.adapter.TweetPresenter; 30 | import org.lucasr.layoutsamples.app.R; 31 | import org.lucasr.layoutsamples.util.ImageUtils; 32 | 33 | import java.util.EnumMap; 34 | import java.util.EnumSet; 35 | 36 | public class TweetCompositeView extends RelativeLayout implements TweetPresenter { 37 | private final ImageView mProfileImage; 38 | private final TextView mAuthorText; 39 | private final TextView mMessageText; 40 | private final ImageView mPostImage; 41 | private final EnumMap mActionIcons; 42 | 43 | public TweetCompositeView(Context context, AttributeSet attrs) { 44 | this(context, attrs, 0); 45 | } 46 | 47 | public TweetCompositeView(Context context, AttributeSet attrs, int defStyleAttr) { 48 | super(context, attrs, defStyleAttr); 49 | 50 | LayoutInflater.from(context).inflate(R.layout.tweet_composite_view, this, true); 51 | mProfileImage = (ImageView) findViewById(R.id.profile_image); 52 | mAuthorText = (TextView) findViewById(R.id.author_text); 53 | mMessageText = (TextView) findViewById(R.id.message_text); 54 | mPostImage = (ImageView) findViewById(R.id.post_image); 55 | 56 | mActionIcons = new EnumMap(Action.class); 57 | for (Action action : Action.values()) { 58 | final ImageView icon; 59 | switch (action) { 60 | case REPLY: 61 | icon = (ImageView) findViewById(R.id.reply_action); 62 | break; 63 | 64 | case RETWEET: 65 | icon = (ImageView) findViewById(R.id.retweet_action); 66 | break; 67 | 68 | case FAVOURITE: 69 | icon = (ImageView) findViewById(R.id.favourite_action); 70 | break; 71 | 72 | default: 73 | throw new IllegalArgumentException("Unrecognized tweet action"); 74 | } 75 | 76 | mActionIcons.put(action, icon); 77 | } 78 | } 79 | 80 | @Override 81 | public boolean shouldDelayChildPressedState() { 82 | return false; 83 | } 84 | 85 | @Override 86 | public void update(Tweet tweet, EnumSet flags) { 87 | mAuthorText.setText(tweet.getAuthorName()); 88 | mMessageText.setText(tweet.getMessage()); 89 | 90 | final Context context = getContext(); 91 | ImageUtils.loadImage(context, mProfileImage, tweet.getProfileImageUrl(), flags); 92 | 93 | final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl()); 94 | mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE); 95 | if (hasPostImage) { 96 | ImageUtils.loadImage(context, mPostImage, tweet.getPostImageUrl(), flags); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/widget/TweetElement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.widget; 18 | 19 | import android.content.res.Resources; 20 | import android.text.TextUtils; 21 | import android.util.AttributeSet; 22 | import android.view.View; 23 | import android.view.View.MeasureSpec; 24 | import android.view.ViewGroup.MarginLayoutParams; 25 | 26 | import com.squareup.picasso.Picasso; 27 | import com.squareup.picasso.Target; 28 | 29 | import org.lucasr.layoutsamples.adapter.Tweet; 30 | import org.lucasr.layoutsamples.adapter.TweetPresenter; 31 | import org.lucasr.layoutsamples.app.R; 32 | import org.lucasr.layoutsamples.canvas.UIElementGroup; 33 | import org.lucasr.layoutsamples.canvas.ImageElement; 34 | import org.lucasr.layoutsamples.canvas.TextElement; 35 | import org.lucasr.layoutsamples.canvas.UIElement; 36 | import org.lucasr.layoutsamples.canvas.UIElementHost; 37 | import org.lucasr.layoutsamples.canvas.UIElementInflater; 38 | import org.lucasr.layoutsamples.util.ImageUtils; 39 | 40 | import java.util.EnumMap; 41 | import java.util.EnumSet; 42 | 43 | public class TweetElement extends UIElementGroup implements TweetPresenter { 44 | private ImageElement mProfileImage; 45 | private TextElement mAuthorText; 46 | private TextElement mMessageText; 47 | private ImageElement mPostImage; 48 | private EnumMap mActionIcons; 49 | 50 | private ImageElementTarget mProfileImageTarget; 51 | private ImageElementTarget mPostImageTarget; 52 | 53 | public TweetElement(UIElementHost host) { 54 | this(host, null); 55 | } 56 | 57 | public TweetElement(UIElementHost host, AttributeSet attrs) { 58 | super(host, attrs); 59 | 60 | final Resources res = getResources(); 61 | 62 | int padding = res.getDimensionPixelOffset(R.dimen.tweet_padding); 63 | setPadding(padding, padding, padding, padding); 64 | 65 | UIElementInflater.from(getContext()).inflate(R.layout.tweet_element_view, host, this); 66 | mProfileImage = (ImageElement) findElementById(R.id.profile_image); 67 | mAuthorText = (TextElement) findElementById(R.id.author_text); 68 | mMessageText = (TextElement) findElementById(R.id.message_text); 69 | mPostImage = (ImageElement) findElementById(R.id.post_image); 70 | 71 | mProfileImageTarget = new ImageElementTarget(res, mProfileImage); 72 | mPostImageTarget = new ImageElementTarget(res, mPostImage); 73 | 74 | mActionIcons = new EnumMap(Action.class); 75 | for (Action action : Action.values()) { 76 | final int elementId; 77 | switch (action) { 78 | case REPLY: 79 | elementId = R.id.reply_action; 80 | break; 81 | 82 | case RETWEET: 83 | elementId = R.id.retweet_action; 84 | break; 85 | 86 | case FAVOURITE: 87 | elementId = R.id.favourite_action; 88 | break; 89 | 90 | default: 91 | throw new IllegalArgumentException("Unrecognized tweet action"); 92 | } 93 | 94 | mActionIcons.put(action, findElementById(elementId)); 95 | } 96 | } 97 | 98 | private void layoutElement(UIElement element, int left, int top, int width, int height) { 99 | MarginLayoutParams margins = (MarginLayoutParams) element.getLayoutParams(); 100 | final int leftWithMargins = left + margins.leftMargin; 101 | final int topWithMargins = top + margins.topMargin; 102 | 103 | element.layout(leftWithMargins, topWithMargins, 104 | leftWithMargins + width, topWithMargins + height); 105 | } 106 | 107 | private int getWidthWithMargins(UIElement element) { 108 | final MarginLayoutParams lp = (MarginLayoutParams) element.getLayoutParams(); 109 | return element.getWidth() + lp.leftMargin + lp.rightMargin; 110 | } 111 | 112 | private int getHeightWithMargins(UIElement element) { 113 | final MarginLayoutParams lp = (MarginLayoutParams) element.getLayoutParams(); 114 | return element.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 115 | } 116 | 117 | private int getMeasuredWidthWithMargins(UIElement element) { 118 | final MarginLayoutParams lp = (MarginLayoutParams) element.getLayoutParams(); 119 | return element.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; 120 | } 121 | 122 | private int getMeasuredHeightWithMargins(UIElement element) { 123 | final MarginLayoutParams lp = (MarginLayoutParams) element.getLayoutParams(); 124 | return element.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 125 | } 126 | 127 | private void cancelImageRequest(Target target) { 128 | if (!isAttachedToHost() || target == null) { 129 | return; 130 | } 131 | 132 | Picasso.with(getContext()).cancelRequest(target); 133 | } 134 | 135 | @Override 136 | public boolean swapHost(UIElementHost host) { 137 | if (host == null) { 138 | cancelImageRequest(mProfileImageTarget); 139 | cancelImageRequest(mPostImageTarget); 140 | } 141 | 142 | return super.swapHost(host); 143 | } 144 | 145 | @Override 146 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 147 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 148 | 149 | int widthUsed = 0; 150 | int heightUsed = 0; 151 | 152 | measureElementWithMargins(mProfileImage, 153 | widthMeasureSpec, widthUsed, 154 | heightMeasureSpec, heightUsed); 155 | widthUsed += getMeasuredWidthWithMargins(mProfileImage); 156 | 157 | measureElementWithMargins(mAuthorText, 158 | widthMeasureSpec, widthUsed, 159 | heightMeasureSpec, heightUsed); 160 | heightUsed += getMeasuredHeightWithMargins(mAuthorText); 161 | 162 | measureElementWithMargins(mMessageText, 163 | widthMeasureSpec, widthUsed, 164 | heightMeasureSpec, heightUsed); 165 | heightUsed += getMeasuredHeightWithMargins(mMessageText); 166 | 167 | if (mPostImage.getVisibility() != View.GONE) { 168 | measureElementWithMargins(mPostImage, 169 | widthMeasureSpec, widthUsed, 170 | heightMeasureSpec, heightUsed); 171 | heightUsed += getMeasuredHeightWithMargins(mPostImage); 172 | } 173 | 174 | int maxIconHeight = 0; 175 | for (Action action : Action.values()) { 176 | final UIElement icon = mActionIcons.get(action); 177 | measureElementWithMargins(icon, 178 | widthMeasureSpec, widthUsed, 179 | heightMeasureSpec, heightUsed); 180 | 181 | final int height = getMeasuredHeightWithMargins(icon); 182 | if (height > maxIconHeight) { 183 | maxIconHeight = height; 184 | } 185 | 186 | widthUsed += getMeasuredWidthWithMargins(icon); 187 | } 188 | heightUsed += maxIconHeight; 189 | 190 | int heightSize = heightUsed + getPaddingTop() + getPaddingBottom(); 191 | setMeasuredDimension(widthSize, heightSize); 192 | } 193 | 194 | @Override 195 | public void onLayout(int l, int t, int r, int b) { 196 | final int paddingLeft = getPaddingLeft(); 197 | final int paddingTop = getPaddingTop(); 198 | 199 | int currentTop = paddingTop; 200 | 201 | layoutElement(mProfileImage, paddingLeft, currentTop, 202 | mProfileImage.getMeasuredWidth(), 203 | mProfileImage.getMeasuredHeight()); 204 | 205 | final int contentLeft = getWidthWithMargins(mProfileImage) + paddingLeft; 206 | final int contentWidth = r - l - contentLeft - getPaddingRight(); 207 | 208 | layoutElement(mAuthorText, contentLeft, currentTop, 209 | contentWidth, mAuthorText.getMeasuredHeight()); 210 | currentTop += getHeightWithMargins(mAuthorText); 211 | 212 | layoutElement(mMessageText, contentLeft, currentTop, 213 | contentWidth, mMessageText.getMeasuredHeight()); 214 | currentTop += getHeightWithMargins(mMessageText); 215 | 216 | if (mPostImage.getVisibility() != View.GONE) { 217 | layoutElement(mPostImage, contentLeft, currentTop, 218 | contentWidth, mPostImage.getMeasuredHeight()); 219 | 220 | currentTop += getHeightWithMargins(mPostImage); 221 | } 222 | 223 | final int iconsWidth = contentWidth / mActionIcons.size(); 224 | int iconsLeft = contentLeft; 225 | 226 | for (Action action : Action.values()) { 227 | final UIElement icon = mActionIcons.get(action); 228 | 229 | layoutElement(icon, iconsLeft, currentTop, 230 | iconsWidth, icon.getMeasuredHeight()); 231 | iconsLeft += iconsWidth; 232 | } 233 | } 234 | 235 | public void loadProfileImage(Tweet tweet, EnumSet flags) { 236 | ImageUtils.loadImage(getContext(), mProfileImage, mProfileImageTarget, 237 | tweet.getProfileImageUrl(), flags); 238 | } 239 | 240 | public void loadPostImage(Tweet tweet, EnumSet flags) { 241 | ImageUtils.loadImage(getContext(), mPostImage, mPostImageTarget, 242 | tweet.getPostImageUrl(), flags); 243 | } 244 | 245 | @Override 246 | public void update(Tweet tweet, EnumSet flags) { 247 | mAuthorText.setText(tweet.getAuthorName()); 248 | mMessageText.setText(tweet.getMessage()); 249 | 250 | loadProfileImage(tweet, flags); 251 | 252 | final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl()); 253 | mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE); 254 | if (hasPostImage) { 255 | loadPostImage(tweet, flags); 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/widget/TweetElementView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.widget; 18 | 19 | import android.content.Context; 20 | import android.graphics.drawable.Drawable; 21 | import android.util.AttributeSet; 22 | 23 | import org.lucasr.layoutsamples.adapter.Tweet; 24 | import org.lucasr.layoutsamples.adapter.TweetPresenter; 25 | import org.lucasr.layoutsamples.canvas.UIElementView; 26 | 27 | import java.util.EnumSet; 28 | 29 | public class TweetElementView extends UIElementView implements TweetPresenter { 30 | public TweetElementView(Context context, AttributeSet attrs) { 31 | this(context, attrs, 0); 32 | } 33 | 34 | public TweetElementView(Context context, AttributeSet attrs, int defStyleAttr) { 35 | super(context, attrs, defStyleAttr); 36 | setUIElement(new TweetElement(this)); 37 | } 38 | 39 | @Override 40 | public void update(Tweet tweet, EnumSet flags) { 41 | TweetElement element = (TweetElement) getUIElement(); 42 | element.update(tweet, flags); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/lucasr/layoutsamples/widget/TweetLayoutView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Lucas Rocha 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.lucasr.layoutsamples.widget; 18 | 19 | import android.content.Context; 20 | import android.text.TextUtils; 21 | import android.util.AttributeSet; 22 | import android.view.LayoutInflater; 23 | import android.view.View; 24 | import android.view.ViewGroup; 25 | import android.widget.ImageView; 26 | import android.widget.TextView; 27 | 28 | import org.lucasr.layoutsamples.adapter.Tweet; 29 | import org.lucasr.layoutsamples.adapter.TweetPresenter; 30 | import org.lucasr.layoutsamples.app.R; 31 | import org.lucasr.layoutsamples.util.ImageUtils; 32 | 33 | import java.util.EnumMap; 34 | import java.util.EnumSet; 35 | 36 | public class TweetLayoutView extends ViewGroup implements TweetPresenter { 37 | private final ImageView mProfileImage; 38 | private final TextView mAuthorText; 39 | private final TextView mMessageText; 40 | private final ImageView mPostImage; 41 | private final EnumMap mActionIcons; 42 | 43 | public TweetLayoutView(Context context, AttributeSet attrs) { 44 | this(context, attrs, 0); 45 | } 46 | 47 | public TweetLayoutView(Context context, AttributeSet attrs, int defStyleAttr) { 48 | super(context, attrs, defStyleAttr); 49 | 50 | LayoutInflater.from(context).inflate(R.layout.tweet_layout_view, this, true); 51 | mProfileImage = (ImageView) findViewById(R.id.profile_image); 52 | mAuthorText = (TextView) findViewById(R.id.author_text); 53 | mMessageText = (TextView) findViewById(R.id.message_text); 54 | mPostImage = (ImageView) findViewById(R.id.post_image); 55 | 56 | mActionIcons = new EnumMap(Action.class); 57 | for (Action action : Action.values()) { 58 | final int viewId; 59 | switch (action) { 60 | case REPLY: 61 | viewId = R.id.reply_action; 62 | break; 63 | 64 | case RETWEET: 65 | viewId = R.id.retweet_action; 66 | break; 67 | 68 | case FAVOURITE: 69 | viewId = R.id.favourite_action; 70 | break; 71 | 72 | default: 73 | throw new IllegalArgumentException("Unrecognized tweet action"); 74 | } 75 | 76 | mActionIcons.put(action, findViewById(viewId)); 77 | } 78 | } 79 | 80 | private void layoutView(View view, int left, int top, int width, int height) { 81 | MarginLayoutParams margins = (MarginLayoutParams) view.getLayoutParams(); 82 | final int leftWithMargins = left + margins.leftMargin; 83 | final int topWithMargins = top + margins.topMargin; 84 | 85 | view.layout(leftWithMargins, topWithMargins, 86 | leftWithMargins + width, topWithMargins + height); 87 | } 88 | 89 | private int getWidthWithMargins(View child) { 90 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 91 | return child.getWidth() + lp.leftMargin + lp.rightMargin; 92 | } 93 | 94 | private int getHeightWithMargins(View child) { 95 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 96 | return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 97 | } 98 | 99 | private int getMeasuredWidthWithMargins(View child) { 100 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 101 | return child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; 102 | } 103 | 104 | private int getMeasuredHeightWithMargins(View child) { 105 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 106 | return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 107 | } 108 | 109 | @Override 110 | public boolean shouldDelayChildPressedState() { 111 | return false; 112 | } 113 | 114 | @Override 115 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 116 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 117 | 118 | int widthUsed = 0; 119 | int heightUsed = 0; 120 | 121 | measureChildWithMargins(mProfileImage, 122 | widthMeasureSpec, widthUsed, 123 | heightMeasureSpec, heightUsed); 124 | widthUsed += getMeasuredWidthWithMargins(mProfileImage); 125 | 126 | measureChildWithMargins(mAuthorText, 127 | widthMeasureSpec, widthUsed, 128 | heightMeasureSpec, heightUsed); 129 | heightUsed += getMeasuredHeightWithMargins(mAuthorText); 130 | 131 | measureChildWithMargins(mMessageText, 132 | widthMeasureSpec, widthUsed, 133 | heightMeasureSpec, heightUsed); 134 | heightUsed += getMeasuredHeightWithMargins(mMessageText); 135 | 136 | if (mPostImage.getVisibility() != View.GONE) { 137 | measureChildWithMargins(mPostImage, 138 | widthMeasureSpec, widthUsed, 139 | heightMeasureSpec, heightUsed); 140 | heightUsed += getMeasuredHeightWithMargins(mPostImage); 141 | } 142 | 143 | int maxIconHeight = 0; 144 | for (Action action : Action.values()) { 145 | final View iconView = mActionIcons.get(action); 146 | measureChildWithMargins(iconView, 147 | widthMeasureSpec, widthUsed, 148 | heightMeasureSpec, heightUsed); 149 | 150 | final int height = getMeasuredHeightWithMargins(iconView); 151 | if (height > maxIconHeight) { 152 | maxIconHeight = height; 153 | } 154 | 155 | widthUsed += getMeasuredWidthWithMargins(iconView); 156 | } 157 | heightUsed += maxIconHeight; 158 | 159 | int heightSize = heightUsed + getPaddingTop() + getPaddingBottom(); 160 | setMeasuredDimension(widthSize, heightSize); 161 | } 162 | 163 | @Override 164 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 165 | final int paddingLeft = getPaddingLeft(); 166 | final int paddingTop = getPaddingTop(); 167 | 168 | int currentTop = paddingTop; 169 | 170 | layoutView(mProfileImage, paddingLeft, currentTop, 171 | mProfileImage.getMeasuredWidth(), 172 | mProfileImage.getMeasuredHeight()); 173 | 174 | final int contentLeft = getWidthWithMargins(mProfileImage) + paddingLeft; 175 | final int contentWidth = r - l - contentLeft - getPaddingRight(); 176 | 177 | layoutView(mAuthorText, contentLeft, currentTop, 178 | contentWidth, mAuthorText.getMeasuredHeight()); 179 | currentTop += getHeightWithMargins(mAuthorText); 180 | 181 | layoutView(mMessageText, contentLeft, currentTop, 182 | contentWidth, mMessageText.getMeasuredHeight()); 183 | currentTop += getHeightWithMargins(mMessageText); 184 | 185 | if (mPostImage.getVisibility() != View.GONE) { 186 | layoutView(mPostImage, contentLeft, currentTop, 187 | contentWidth, mPostImage.getMeasuredHeight()); 188 | 189 | currentTop += getHeightWithMargins(mPostImage); 190 | } 191 | 192 | final int iconsWidth = contentWidth / mActionIcons.size(); 193 | int iconsLeft = contentLeft; 194 | 195 | for (Action action : Action.values()) { 196 | final View icon = mActionIcons.get(action); 197 | 198 | layoutView(icon, iconsLeft, currentTop, 199 | iconsWidth, icon.getMeasuredHeight()); 200 | iconsLeft += iconsWidth; 201 | } 202 | } 203 | 204 | @Override 205 | public LayoutParams generateLayoutParams(AttributeSet attrs) { 206 | return new MarginLayoutParams(getContext(), attrs); 207 | } 208 | 209 | @Override 210 | protected LayoutParams generateDefaultLayoutParams() { 211 | return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 212 | } 213 | 214 | @Override 215 | public void update(Tweet tweet, EnumSet flags) { 216 | mAuthorText.setText(tweet.getAuthorName()); 217 | mMessageText.setText(tweet.getMessage()); 218 | 219 | final Context context = getContext(); 220 | ImageUtils.loadImage(context, mProfileImage, tweet.getProfileImageUrl(), flags); 221 | 222 | final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl()); 223 | mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE); 224 | if (hasPostImage) { 225 | ImageUtils.loadImage(context, mPostImage, tweet.getPostImageUrl(), flags); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/android-layout-samples/d0fa72fd0fe46788d514db9432e20795e0d2ee25/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/android-layout-samples/d0fa72fd0fe46788d514db9432e20795e0d2ee25/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/tweet_favourite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/android-layout-samples/d0fa72fd0fe46788d514db9432e20795e0d2ee25/src/main/res/drawable-mdpi/tweet_favourite.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/tweet_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/android-layout-samples/d0fa72fd0fe46788d514db9432e20795e0d2ee25/src/main/res/drawable-mdpi/tweet_reply.png -------------------------------------------------------------------------------- /src/main/res/drawable-mdpi/tweet_retweet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/android-layout-samples/d0fa72fd0fe46788d514db9432e20795e0d2ee25/src/main/res/drawable-mdpi/tweet_retweet.png -------------------------------------------------------------------------------- /src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/android-layout-samples/d0fa72fd0fe46788d514db9432e20795e0d2ee25/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasr/android-layout-samples/d0fa72fd0fe46788d514db9432e20795e0d2ee25/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 25 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/res/layout/fragment_tweets.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /src/main/res/layout/tweet_async_row.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/res/layout/tweet_composite_row.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/res/layout/tweet_composite_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 28 | 29 | 38 | 39 | 47 | 48 | 56 | 57 | 63 | 64 | 71 | 72 | 79 | 80 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/main/res/layout/tweet_element_row.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/res/layout/tweet_element_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 28 | 29 | 37 | 38 | 45 | 46 | 52 | 53 | 59 | 60 | 66 | 67 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/res/layout/tweet_layout_row.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/res/layout/tweet_layout_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 28 | 29 | 36 | 37 | 44 | 45 | 51 | 52 | 58 | 59 | 65 | 66 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | #3FBA91 21 | 22 | #DEDEDE 23 | #999999 24 | 25 | #999999 26 | #333333 27 | 28 | #FF0000 29 | #66CD00 30 | 31 | #dddddd 32 | 33 | -------------------------------------------------------------------------------- /src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 16dp 20 | 16dp 21 | 22 | 32dp 23 | 16sp 24 | 25 | 16sp 26 | 16sp 27 | 28 | 10dp 29 | 10dp 30 | 40dp 31 | 32 | 60dp 33 | 130dp 34 | 20dp 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | Layout Samples 21 | Composite 22 | Layout 23 | Element 24 | Async 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 24 | 25 | 29 | 30 | --------------------------------------------------------------------------------