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 |
--------------------------------------------------------------------------------